package checker import "../ast" import "../hir" import "../source" import "../symbol" import "../target" import "../types" import "core:fmt" import "core:mem" import "core:slice" import "core:strings" Spec_Id :: distinct u32 INVALID_SPEC :: Spec_Id(0xffff_ffff) spec_id :: proc(index: int) -> Spec_Id { assert(index >= 0 && u64(index) < u64(INVALID_SPEC)) return Spec_Id(index) } spec_index :: proc(id: Spec_Id, count: int) -> (int, bool) { index := int(id) return index, id != INVALID_SPEC && index < count } Spec :: struct { template: ast.Function_Id, args: []types.Type, comptime_values: []Comptime_Value, result: types.Type, hir_id: hir.Function_Id, } Infer_Local :: struct { name: symbol.Id, type: types.Type, declared: types.Type, statement: ast.Stmt_Id, mutable: bool, // open_const/open_float mark a local whose initializer is an unannotated numeric // constant: like an open-constant global, it can adopt a backward demand from use. open_const: bool, open_float: bool, const_value: i128, demanded: bool, } Build_Local :: struct { name: symbol.Id, type: types.Type, mutable: bool, id: hir.Local_Id, } // Build_Ctx threads the per-function accumulators through build_block so that // nested control-flow blocks (if/else) can be built recursively. `locals` is a // scope stack: each block records its entry length and truncates back to it on // exit, while `hir_locals` keeps every allocated slot for the function. // A labeled value-loop currently being built. A `yield :label x` inside the loop // body assigns `x` to the loop's result `slot` (typed `slot_type`) and `break`s. // Pushed by `build_value_loop` while its body is built; innermost is last. Yield_Target :: struct { label: symbol.Id, slot: hir.Local_Id, slot_type: types.Type, // True when the loop also yields `none` (a `{T, none}` set → `?T`); set from a // pure-AST scan, used to pick the slot's element type on the first concrete yield. result_optional: bool, // `len(defers)` when this target's body began; a `yield :label` flushes defers down // to here before breaking, so an outer-loop / value-block yield runs inner defers too. defer_floor: int, } Build_Ctx :: struct { checker: ^Checker, pkg: ast.Package_Id, file: ast.File_Id, result: types.Type, local_types: []types.Type, locals: ^[dynamic]Build_Local, hir_locals: ^[dynamic]hir.Local, local_spans: ^[dynamic]source.Span, local_used: ^[dynamic]bool, local_warnable: ^[dynamic]bool, global_reads: ^[dynamic]hir.Global_Id, calls: ^[dynamic]hir.Function_Id, problematic: ^bool, // Stack of labeled value-loops being built (innermost last); see Yield_Target. yield_targets: ^[dynamic]Yield_Target, // `defer` lowering. Deferred statements are built once at the `defer` site and // their hir stmt ids stored here as a flat stack across scopes (one entry per // deferred statement); they are replayed (appended) at each scope exit in LIFO // order. `loop_defer_starts` records `len(defers)` at each enclosing loop body // entry: `break`/`continue` flush down to that mark (and need `len > loop_floor` // to be valid). `defer_depth`/`loop_floor` guard control flow inside a deferred // statement: `return` is rejected while `defer_depth > 0`, and `break`/`continue` // only see loops opened within the defer (those past `loop_floor`). defers: ^[dynamic][]hir.Stmt_Id, loop_defer_starts: ^[dynamic]int, // Parallel to `loop_defer_starts`: the label of each enclosing break target (INVALID // when unlabeled), so a `break :L` / `continue :L` can target an outer one. A labeled // block statement is a break target too; `loop_is_loop` distinguishes loops (which // `continue` and unlabeled `break`/`continue` target) from value/labeled blocks. loop_labels: ^[dynamic]symbol.Id, loop_is_loop: ^[dynamic]bool, defer_depth: int, loop_floor: int, } Function_Index_Entry :: struct { scope: ast.Package_Id, file: ast.File_Id, hidden: bool, name: symbol.Id, id: ast.Function_Id, } Global_Index_Entry :: struct { scope: ast.Package_Id, file: ast.File_Id, hidden: bool, name: symbol.Id, id: ast.Global_Id, } Import_Index_Entry :: struct { scope: ast.File_Id, name: symbol.Id, id: ast.Import_Id, } Type_Factory_Entry :: struct { template: ast.Function_Id, values: []Comptime_Value, result: types.Type, resolving: bool, } Generated_Type_Entry :: struct { expr: ast.Expr_Id, values: []Comptime_Value, result: types.Type, } Type_Factory_Origin :: struct { result: types.Type, template: ast.Function_Id, values: []Comptime_Value, } Checker :: struct { ast_module: ^ast.Module, diagnostics: ^source.Diagnostics, symbols: ^symbol.Table, module: hir.Module, specs: [dynamic]Spec, function_index: []Function_Index_Entry, global_index: []Global_Index_Entry, import_index: []Import_Index_Entry, global_types: []types.Type, // Backward type-demand state for open-constant globals (milestone 14). global_demands // accumulates demands reachable from any use (other globals' initializers and function // bodies); global_demands_dirty lets a demand pushed from a function body re-trigger the // inference fixpoint. global_demands: []types.Type, global_open_const: []bool, global_open_float: []bool, global_const_value: []i128, global_demands_dirty: bool, external_global_canonical: []ast.Global_Id, external_global_diagnostics: []source.Diagnostic_Id, constants: []Constant, template_diagnostics: []source.Diagnostic_Id, constant_stack: [dynamic]Constant_Frame, ast_expr_stack: [dynamic]ast.Expr_Id, hir_expr_stack: [dynamic]hir.Expr_Id, infer_stack: [dynamic]Infer_Frame, build_stack: [dynamic]Build_Expr_Frame, cycle_stack: [dynamic]Cycle_Frame, // Anonymous globals synthesized for `&` (Zig's `&.{...}`). Staged // here during global/function building and flushed into module.globals AFTER // build_globals, so the 1:1 module.globals <-> ast.globals index identity holds. anon_globals: [dynamic]hir.Global, main_symbol: symbol.Id, sink_symbol: symbol.Id, type_symbol: symbol.Id, current_result: types.Type, current_build_ctx: ^Build_Ctx, current_comptime_values: []Comptime_Value, type_factories: [dynamic]Type_Factory_Entry, generated_types: [dynamic]Generated_Type_Entry, type_factory_origins: [dynamic]Type_Factory_Origin, target: target.Target, allocator: mem.Allocator, } symbol_text :: proc(checker: ^Checker, id: symbol.Id) -> string { return symbol.resolve(checker.symbols, id) } append_tracked_local :: proc( locals: ^[dynamic]hir.Local, spans: ^[dynamic]source.Span, used: ^[dynamic]bool, warnable: ^[dynamic]bool, local: hir.Local, span: source.Span, ) -> hir.Local_Id { id := hir.local_id(len(locals^)) append(locals, local) append(spans, span) append(used, false) append(warnable, true) return id } append_build_local :: proc( ctx: ^Build_Ctx, name: symbol.Id, type: types.Type, mutable: bool, span: source.Span, ) -> hir.Local_Id { id := append_tracked_local( ctx.hir_locals, ctx.local_spans, ctx.local_used, ctx.local_warnable, hir.Local{name=name, type=type, mutable=mutable}, span, ) append(ctx.locals, Build_Local{name=name, type=type, mutable=mutable, id=id}) return id } ignore_tracked_locals :: proc(ctx: ^Build_Ctx, start: int) { for i := start; i < len(ctx.local_warnable^); i += 1 { ctx.local_warnable^[i] = false } } mark_local_used :: proc(checker: ^Checker, id: hir.Local_Id) { ctx := checker.current_build_ctx if ctx == nil || id == hir.INVALID_LOCAL { return } index := int(id) if index >= 0 && index < len(ctx.local_used^) { ctx.local_used^[index] = true } } build_local_expr :: proc(checker: ^Checker, local: Build_Local, span: source.Span) -> hir.Expr_Id { mark_local_used(checker, local.id) return add_hir_expr(checker, hir.Expr{ kind=.Local, span=span, type=local.type, target=hir.local_ref(local.id), left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) } record_unused_locals :: proc( checker: ^Checker, locals: []hir.Local, spans: []source.Span, used: []bool, warnable: []bool, ) { for local, index in locals { if local.name == checker.sink_symbol || !symbol.is_valid(local.name) || index >= len(warnable) || !warnable[index] || index >= len(used) || used[index] { continue } span := source.Span{} if index < len(spans) { span = spans[index] } if local.parameter { source.addf_warning(checker.diagnostics, span, "unused parameter '%s'", symbol_text(checker, local.name)) } else { source.addf_warning(checker.diagnostics, span, "unused local '%s'", symbol_text(checker, local.name)) } } } write_type_label :: proc(checker: ^Checker, builder: ^strings.Builder, value: types.Type) { store := &checker.module.types item, ok := types.node(store, value) if !ok { strings.write_string(builder, types.name(value)) return } if item.name != 0 { strings.write_string(builder, symbol_text(checker, symbol.Id(item.name))) return } switch item.kind { case .Array: strings.write_byte(builder, '[') if item.inferred_count { strings.write_byte(builder, '_') } else { fmt.sbprintf(builder, "%d", item.count) } if item.has_sentinel { fmt.sbprintf(builder, ";%d", item.sentinel) } strings.write_byte(builder, ']') if item.mutable { strings.write_string(builder, "mut ") } write_type_label(checker, builder, item.child) case .Pointer: if item.has_sentinel { fmt.sbprintf(builder, "[*;%d]", item.sentinel) } else { strings.write_byte(builder, '*' if item.many else '@') } if item.mutable { strings.write_string(builder, "mut ") } write_type_label(checker, builder, item.child) case .Slice: if item.has_sentinel { fmt.sbprintf(builder, "[;%d]", item.sentinel) } else { strings.write_string(builder, "[]") } if item.mutable { strings.write_string(builder, "mut ") } write_type_label(checker, builder, item.child) case .Range: strings.write_string(builder, "range(") write_type_label(checker, builder, item.child) strings.write_byte(builder, ')') case .Optional: strings.write_byte(builder, '?') write_type_label(checker, builder, item.child) case .Function: strings.write_string(builder, "c_func(" if item.c_abi else "func(") for param, index in types.params_for(store, value) { if index > 0 { strings.write_string(builder, ", ") } write_type_label(checker, builder, param.type) } if item.variadic { if item.field_count > 0 { strings.write_string(builder, ", ") } strings.write_string(builder, "...") } strings.write_string(builder, ") ") write_type_label(checker, builder, item.child) case .Fallible: write_type_label(checker, builder, item.child) strings.write_string(builder, " ! ") write_type_label(checker, builder, item.extra) case .Type_Call: strings.write_string(builder, "") case .Struct: strings.write_string(builder, "struct") case .Union: strings.write_string(builder, "union") case .Enum: strings.write_string(builder, "enum") case .Alias, .Distinct, .Named: write_type_label(checker, builder, item.child) case .Invalid, .Void, .Anyopaque, .Int_Constraint, .Float_Constraint, .Range_Constraint, .Scalar: strings.write_string(builder, types.name(value)) } } // Render dynamic types using source syntax so diagnostics never expose internal // type-store ids such as ``. type_label :: proc(checker: ^Checker, value: types.Type) -> string { builder := strings.builder_make(context.temp_allocator) write_type_label(checker, &builder, value) return strings.to_string(builder) } is_ptr_cast_call :: proc(checker: ^Checker, expr: ast.Expr) -> bool { return expr.left == ast.INVALID_EXPR && !symbol.is_valid(expr.qualifier) && symbol_text(checker, expr.name) == "ptr_cast" } Type_Builtin :: enum u8 { None, Size_Of, Align_Of, Min_Value, Max_Value, } type_builtin_call :: proc(checker: ^Checker, expr: ast.Expr) -> Type_Builtin { if expr.kind != .Call || expr.left != ast.INVALID_EXPR || symbol.is_valid(expr.qualifier) { return .None } name := symbol_text(checker, expr.name) if name == "size_of" { return .Size_Of } if name == "align_of" { return .Align_Of } if name == "min_value" { return .Min_Value } if name == "max_value" { return .Max_Value } return .None } valid_ptr_cast_child :: proc(checker: ^Checker, value: types.Type) -> bool { return types.is_valid(value) && !types.is_void(value) && !types.is_anyopaque(value) && !types.is_function(value, &checker.module.types) && (types.is_runtime_value(value, &checker.module.types) || types.is_opaque_struct(value, &checker.module.types)) } valid_layout_type :: proc(checker: ^Checker, value: types.Type) -> bool { return types.is_runtime_value(value, &checker.module.types) } type_builtin_value :: proc(checker: ^Checker, kind: Type_Builtin, value: types.Type) -> i128 { #partial switch kind { case .Size_Of: return i128(types.size(value, &checker.module.types, checker.target)) case .Align_Of: return i128(types.alignment_of(value, &checker.module.types, checker.target)) case .Min_Value: if types.is_unsigned(value, checker.target) { return 0 } return -(i128(1) << u32(types.bits(value, checker.target)-1)) case .Max_Value: bit_count := types.bits(value, checker.target) sign_bit_count := 1 if types.is_signed(value, checker.target) else 0 return (i128(1) << u32(bit_count-sign_bit_count))-1 case: return 0 } } build_type_builtin :: proc( checker: ^Checker, expr: ast.Expr, kind: Type_Builtin, pkg: ast.Package_Id, file: ast.File_Id, ) -> hir.Expr_Id { if len(expr.args) != 1 { id := source.addf(checker.diagnostics, expr.span, "%s expects 1 argument, got %d", symbol_text(checker, expr.name), len(expr.args)) return invalid_hir_expr(checker, expr.span, id, types.USIZE) } target, target_ok := resolve_type_argument(checker, expr.args[0], pkg, file) if !target_ok { label := "layout" if kind == .Size_Of || kind == .Align_Of else "integer bound" id := source.addf(checker.diagnostics, checker.ast_module.exprs[expr.args[0]].span, "%s target must be a type", label) return invalid_hir_expr(checker, expr.span, id, types.USIZE) } if (kind == .Size_Of || kind == .Align_Of) && !valid_layout_type(checker, target) { id := source.addf(checker.diagnostics, checker.ast_module.exprs[expr.args[0]].span, "layout target must be a sized runtime value type, got %s", type_label(checker, target)) return invalid_hir_expr(checker, expr.span, id, types.USIZE) } if (kind == .Min_Value || kind == .Max_Value) && !types.is_concrete_integer(target) { id := source.addf(checker.diagnostics, checker.ast_module.exprs[expr.args[0]].span, "integer bound target must be a concrete integer type, got %s", type_label(checker, target)) return invalid_hir_expr(checker, expr.span, id, types.USIZE) } result_type := types.USIZE if kind == .Size_Of || kind == .Align_Of else target return build_constant_expr( checker, expr, Constant{kind=.Value, value=type_builtin_value(checker, kind, target)}, result_type, ) } is_type_metatype_syntax :: proc(checker: ^Checker, value: ast.Type_Syntax) -> bool { item, ok := types.node(&checker.module.types, value) return ok && item.name == u32(checker.type_symbol) && item.qualifier == 0 } is_comptime_type_param :: proc(checker: ^Checker, param: ast.Param) -> bool { return param.comptime_value && is_type_metatype_syntax(checker, param.type) } function_has_comptime_params :: proc(function: ast.Function) -> bool { for param in function.params { if param.comptime_value { return true } } return false } runtime_param_count :: proc(function: ast.Function) -> int { count := 0 for param in function.params { if !param.comptime_value { count += 1 } } return count } comptime_param_count :: proc(function: ast.Function) -> int { count := 0 for param in function.params { if param.comptime_value { count += 1 } } return count } fits_signed_type :: proc(value: i128, value_type: types.Type, selected := target.DEFAULT) -> bool { if !types.is_signed(value_type, selected) { return false } limit := i128(1) << u32(types.bits(value_type, selected) - 1) return value >= -limit && value < limit } fits_unsigned_type :: proc(value: i128, value_type: types.Type, selected := target.DEFAULT) -> bool { if !types.is_unsigned(value_type, selected) || value < 0 { return false } limit := i128(1) << u32(types.bits(value_type, selected)) return value < limit } fits_integer_type :: proc(value: i128, value_type: types.Type, selected := target.DEFAULT) -> bool { return fits_signed_type(value, value_type, selected) || fits_unsigned_type(value, value_type, selected) } fits_i64 :: proc(value: i128) -> bool { return fits_signed_type(value, types.I64) } type_from_syntax :: proc( checker: ^Checker, value: ast.Type_Syntax, pkg := ast.Package_Id(0), file := ast.File_Id(0), depth := 0, ) -> types.Type { if depth > 64 { return types.INVALID } item, ok := types.node(&checker.module.types, value) if !ok { return value } if item.qualifier == 0 && item.name != 0 { if actual, ok := current_comptime_type(checker, symbol.Id(item.name)); ok { return actual } } store := &checker.module.types changed := false #partial switch item.kind { case .Alias: return type_from_syntax(checker, item.child, pkg, file, depth+1) case .Array: child := type_from_syntax(checker, item.child, pkg, file, depth+1) changed = changed || child != item.child item.child = child if item.unresolved_count { expr_id := ast.Expr_Id(item.count_expr) span := source.Span{} if expr_id != ast.INVALID_EXPR && int(expr_id) < len(checker.ast_module.exprs) { span = checker.ast_module.exprs[expr_id].span } constant := eval_integer_constant_in_context(checker, expr_id, pkg, file) if constant.kind == .Value { switch { case constant.value < 0: source.add(checker.diagnostics, span, "array count must be non-negative") return types.INVALID case constant.value > i128(0xffff_ffff_ffff_ffff): source.add(checker.diagnostics, span, "array count does not fit in u64") return types.INVALID case: item.count = u64(constant.value) item.unresolved_count = false item.count_expr = 0 changed = true } } else { source.add(checker.diagnostics, span, "array count must be a compile-time integer expression") return types.INVALID } } case .Pointer, .Slice, .Optional, .Range, .Distinct, .Enum, .Fallible: child := type_from_syntax(checker, item.child, pkg, file, depth+1) extra := type_from_syntax(checker, item.extra, pkg, file, depth+1) changed = child != item.child || extra != item.extra item.child = child item.extra = extra case .Function: params := types.params_for(store, value) resolved_params := make([]types.Type, len(params), checker.allocator) defer delete(resolved_params, checker.allocator) params_changed := false for param, index in params { resolved_params[index] = type_from_syntax(checker, param.type, pkg, file, depth+1) params_changed = params_changed || resolved_params[index] != param.type } result := type_from_syntax(checker, item.child, pkg, file, depth+1) if params_changed || result != item.child { return types.function(store, resolved_params, result, item.c_abi, item.variadic) } case .Type_Call: return resolve_type_factory_call(checker, ast.Expr_Id(item.count_expr), pkg, file) } if changed { return types.intern(store, item) } return value } function_channel_type :: proc(checker: ^Checker, function: ast.Function) -> types.Type { result := type_from_syntax(checker, function.result, function.pkg, function.file) if types.is_valid(function.error) { return types.fallible(&checker.module.types, result, type_from_syntax(checker, function.error, function.pkg, function.file)) } return result } is_runtime_type :: proc(checker: ^Checker, value: types.Type) -> bool { return types.is_runtime_value(value, &checker.module.types) } is_undefined_expr :: proc(checker: ^Checker, expr_id: ast.Expr_Id) -> bool { if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) { return false } return checker.ast_module.exprs[expr_id].kind == .Undefined } is_float_constant_expr :: proc(checker: ^Checker, expr_id: ast.Expr_Id) -> bool { if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) { return false } expr := checker.ast_module.exprs[expr_id] if expr.kind == .Float { return true } return expr.kind == .Negate && is_float_constant_expr(checker, expr.left) } is_numeric_arithmetic_expr :: proc(checker: ^Checker, expr_id: ast.Expr_Id) -> bool { if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) { return false } #partial switch checker.ast_module.exprs[expr_id].kind { case .Add, .Sub, .Mul, .Div, .Negate: return true } return false } is_numeric_constant_expr :: proc(checker: ^Checker, expr_id: ast.Expr_Id) -> bool { if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) { return false } return eval_constant(checker, expr_id).kind == .Value || is_float_constant_expr(checker, expr_id) } is_numeric_demand :: proc(value: types.Type, selected := target.DEFAULT) -> bool { return types.is_concrete_scalar(value) && !types.is_bool(value) || types.is_float(value, selected) } string_literal_type :: proc(checker: ^Checker, string_id: u64) -> types.Type { length: u64 if string_id < u64(len(checker.ast_module.strings)) { length = u64(len(checker.ast_module.strings[string_id])) } array := types.array(&checker.module.types, types.U8, length, false, true, 0) return types.pointer(&checker.module.types, array, false, false) } container_pointer_type :: proc(store: ^types.Store, item: types.Node) -> types.Type { return types.pointer(store, item.child, item.mutable, true, item.has_sentinel, item.sentinel) } resolve_inferred_array :: proc(checker: ^Checker, value: types.Type, expr_id: ast.Expr_Id) -> types.Type { item, ok := types.node(&checker.module.types, value) if !ok || item.kind != .Array || !item.inferred_count || expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) { return value } expr := checker.ast_module.exprs[expr_id] if expr.kind != .Array { return value } return types.with_array_count(&checker.module.types, value, u64(len(expr.args))) } has_inferred_array_count :: proc(checker: ^Checker, value: types.Type) -> bool { item, ok := types.node(&checker.module.types, value) return ok && item.kind == .Array && item.inferred_count } resolve_inferred_array_from_type :: proc(checker: ^Checker, value, inferred: types.Type) -> types.Type { item, ok := types.node(&checker.module.types, value) actual, actual_ok := types.node(&checker.module.types, inferred) if !ok || !actual_ok || item.kind != .Array || actual.kind != .Array || !item.inferred_count { return value } if item.child != actual.child || item.mutable != actual.mutable || item.has_sentinel != actual.has_sentinel || (item.has_sentinel && item.sentinel != actual.sentinel) { return value } return types.with_array_count(&checker.module.types, value, actual.count) } function_index_less :: proc(left, right: Function_Index_Entry) -> bool { if left.scope != right.scope { return left.scope < right.scope } if left.name != right.name { return int(left.name) < int(right.name) } return left.id < right.id } global_index_less :: proc(left, right: Global_Index_Entry) -> bool { if left.scope != right.scope { return left.scope < right.scope } if left.name != right.name { return int(left.name) < int(right.name) } return left.id < right.id } import_index_less :: proc(left, right: Import_Index_Entry) -> bool { if left.scope != right.scope { return left.scope < right.scope } if left.name != right.name { return int(left.name) < int(right.name) } return left.id < right.id } find_function_symbol :: proc(index: []Function_Index_Entry, scope: ast.Package_Id, name: symbol.Id, file := ast.INVALID_FILE) -> ast.Function_Id { low := 0 high := len(index) for low < high { middle := low + (high-low)/2 entry := index[middle] if entry.scope < scope || entry.scope == scope && int(entry.name) < int(name) { low = middle + 1 } else { high = middle } } visible := ast.INVALID_FUNCTION for low < len(index) && index[low].scope == scope && index[low].name == name { entry := index[low] if entry.hidden { if entry.file == file { return entry.id } } else { visible = entry.id } low += 1 } return visible } find_global_symbol :: proc(index: []Global_Index_Entry, scope: ast.Package_Id, name: symbol.Id, file := ast.INVALID_FILE) -> ast.Global_Id { low := 0 high := len(index) for low < high { middle := low + (high-low)/2 entry := index[middle] if entry.scope < scope || entry.scope == scope && int(entry.name) < int(name) { low = middle + 1 } else { high = middle } } visible := ast.INVALID_GLOBAL for low < len(index) && index[low].scope == scope && index[low].name == name { entry := index[low] if entry.hidden { if entry.file == file { return entry.id } } else { visible = entry.id } low += 1 } return visible } find_import_symbol :: proc(index: []Import_Index_Entry, scope: ast.File_Id, name: symbol.Id) -> ast.Import_Id { low := 0 high := len(index) for low < high { middle := low + (high-low)/2 entry := index[middle] if entry.scope < scope || entry.scope == scope && int(entry.name) < int(name) { low = middle + 1 } else { high = middle } } if low < len(index) && index[low].scope == scope && index[low].name == name { return index[low].id } return ast.INVALID_IMPORT } build_symbol_indexes :: proc(checker: ^Checker) { function_count := 0 for function in checker.ast_module.functions { if !function.generated { function_count += 1 } } checker.function_index = make([]Function_Index_Entry, function_count, checker.allocator) function_index := 0 for function, id in checker.ast_module.functions { if function.generated { continue } checker.function_index[function_index] = Function_Index_Entry{scope=function.pkg, file=function.file, hidden=function.file_hidden, name=function.name, id=ast.function_id(id)} function_index += 1 } slice.sort_by(checker.function_index, function_index_less) checker.global_index = make([]Global_Index_Entry, len(checker.ast_module.globals), checker.allocator) for global, id in checker.ast_module.globals { checker.global_index[id] = Global_Index_Entry{scope=global.pkg, file=global.file, hidden=global.file_hidden, name=global.name, id=ast.global_id(id)} } slice.sort_by(checker.global_index, global_index_less) checker.import_index = make([]Import_Index_Entry, len(checker.ast_module.imports), checker.allocator) for import_item, id in checker.ast_module.imports { checker.import_index[id] = Import_Index_Entry{scope=import_item.file, name=import_item.alias, id=ast.import_id(id)} } slice.sort_by(checker.import_index, import_index_less) } find_template :: proc(checker: ^Checker, name: symbol.Id, pkg := ast.Package_Id(0), file := ast.INVALID_FILE) -> ast.Function_Id { return find_function_symbol(checker.function_index, pkg, name, file) } find_global :: proc(checker: ^Checker, name: symbol.Id, pkg := ast.Package_Id(0), file := ast.INVALID_FILE) -> ast.Global_Id { return find_global_symbol(checker.global_index, pkg, name, file) } find_import :: proc(checker: ^Checker, file: ast.File_Id, alias: symbol.Id, mark_used := false) -> ast.Import_Id { id := find_import_symbol(checker.import_index, file, alias) if id != ast.INVALID_IMPORT && mark_used { checker.ast_module.imports[id].used = true } return id } declared_type_named :: proc(checker: ^Checker, pkg: ast.Package_Id, name: symbol.Id, file := ast.INVALID_FILE) -> bool { id := types.find_named(&checker.module.types, u32(pkg), u32(name), file=u32(file)) item, ok := types.node(&checker.module.types, id) return ok && item.declared } declarations_conflict :: proc(left_file: ast.File_Id, left_hidden: bool, right_file: ast.File_Id, right_hidden: bool) -> bool { return left_file == right_file if left_hidden && right_hidden else true } type_declaration_conflicts :: proc(checker: ^Checker, pkg: ast.Package_Id, name: symbol.Id, file: ast.File_Id, hidden: bool) -> bool { for item in checker.module.types.nodes { if item.declared && item.pkg == u32(pkg) && item.name == u32(name) && declarations_conflict(file, hidden, ast.File_Id(item.file), item.file_hidden) { return true } } return false } visible_name_kind :: proc( checker: ^Checker, name: symbol.Id, pkg: ast.Package_Id, file: ast.File_Id, locals: []Build_Local = nil, labels: []symbol.Id = nil, yield_targets: []Yield_Target = nil, ) -> string { if !symbol.is_valid(name) || name == checker.sink_symbol { return "" } if _, ok := current_comptime_value(checker, name); ok { return "comptime parameter" } if _, ok := find_build_local(locals, name); ok { return "local" } for label in labels { if label == name { return "label" } } for target in yield_targets { if target.label == name { return "label" } } if find_import(checker, file, name) != ast.INVALID_IMPORT { return "import" } if find_global(checker, name, pkg, file) != ast.INVALID_GLOBAL { return "global" } if find_template(checker, name, pkg, file) != ast.INVALID_FUNCTION { return "function" } if declared_type_named(checker, pkg, name, file) { return "type" } return "" } add_shadow_diagnostic :: proc( checker: ^Checker, span: source.Span, name: symbol.Id, decl_kind: string, pkg: ast.Package_Id, file: ast.File_Id, locals: []Build_Local = nil, labels: []symbol.Id = nil, yield_targets: []Yield_Target = nil, ) -> source.Diagnostic_Id { kind := visible_name_kind(checker, name, pkg, file, locals, labels, yield_targets) if len(kind) == 0 { return source.INVALID_DIAGNOSTIC } return source.addf( checker.diagnostics, span, "%s '%s' shadows visible %s", decl_kind, symbol_text(checker, name), kind, ) } add_label_shadow_diagnostic :: proc(ctx: ^Build_Ctx, span: source.Span, label: symbol.Id) -> source.Diagnostic_Id { if !symbol.is_valid(label) { return source.INVALID_DIAGNOSTIC } yield_targets := ctx.yield_targets^[:] if len(yield_targets) > 0 && yield_targets[len(yield_targets) - 1].label == label { label_is_active_loop := false for loop_label in ctx.loop_labels^[:] { if loop_label == label { label_is_active_loop = true break } } if !label_is_active_loop { yield_targets = yield_targets[:len(yield_targets) - 1] } } return add_shadow_diagnostic( ctx.checker, span, label, "label", ctx.pkg, ctx.file, ctx.locals^[:], ctx.loop_labels^[:], yield_targets, ) } expr_package :: proc(checker: ^Checker, expr: ast.Expr, pkg: ast.Package_Id, file: ast.File_Id, mark_used := false) -> (ast.Package_Id, bool) { if !symbol.is_valid(expr.qualifier) { return pkg, true } import_id := find_import(checker, file, expr.qualifier, mark_used) if import_id == ast.INVALID_IMPORT { return ast.INVALID_PACKAGE, false } import_item := checker.ast_module.imports[import_id] if import_item.target == ast.INVALID_PACKAGE || int(import_item.target) >= len(checker.ast_module.packages) || !checker.ast_module.packages[import_item.target].available { return import_item.target, false } return import_item.target, true } expr_lookup_file :: proc(expr: ast.Expr, file: ast.File_Id) -> ast.File_Id { return ast.INVALID_FILE if symbol.is_valid(expr.qualifier) else file } add_package_resolution_diagnostic :: proc(checker: ^Checker, expr: ast.Expr, file: ast.File_Id) -> source.Diagnostic_Id { if find_import(checker, file, expr.qualifier) == ast.INVALID_IMPORT { return source.addf(checker.diagnostics, expr.span, "unknown package alias '%s'", symbol_text(checker, expr.qualifier)) } return source.addf(checker.diagnostics, expr.span, "unavailable imported package '%s'", symbol_text(checker, expr.qualifier)) } add_name_resolution_diagnostic :: proc(checker: ^Checker, expr: ast.Expr, target_pkg: ast.Package_Id, file: ast.File_Id) -> source.Diagnostic_Id { if find_template(checker, expr.name, target_pkg, expr_lookup_file(expr, file)) != ast.INVALID_FUNCTION { return source.addf(checker.diagnostics, expr.span, "'%s' is a function, not a global value", symbol_text(checker, expr.name)) } if symbol.is_valid(expr.qualifier) { return source.addf( checker.diagnostics, expr.span, "package '%s' has no member '%s'", symbol_text(checker, expr.qualifier), symbol_text(checker, expr.name), ) } return source.addf(checker.diagnostics, expr.span, "unresolved global '%s'", symbol_text(checker, expr.name)) } add_call_resolution_diagnostic :: proc(checker: ^Checker, expr: ast.Expr, target_pkg: ast.Package_Id, file: ast.File_Id) -> source.Diagnostic_Id { if find_global(checker, expr.name, target_pkg, expr_lookup_file(expr, file)) != ast.INVALID_GLOBAL { return source.addf(checker.diagnostics, expr.span, "'%s' is a global, not a function", symbol_text(checker, expr.name)) } if symbol.is_valid(expr.qualifier) { return source.addf( checker.diagnostics, expr.span, "package '%s' has no member '%s'", symbol_text(checker, expr.qualifier), symbol_text(checker, expr.name), ) } return source.addf(checker.diagnostics, expr.span, "unresolved function '%s'", symbol_text(checker, expr.name)) } find_unsupported :: proc(checker: ^Checker, pkg: ast.Package_Id, name: symbol.Id) -> (ast.Unsupported, bool) { for item in checker.ast_module.unsupported { if item.pkg == pkg && item.name == name { return item, true } } return {}, false } add_unsupported_diagnostic :: proc(checker: ^Checker, span: source.Span, pkg: ast.Package_Id, name: symbol.Id) -> source.Diagnostic_Id { if item, ok := find_unsupported(checker, pkg, name); ok { return source.addf( checker.diagnostics, span, "C declaration '%s' is unavailable: %s", symbol_text(checker, name), item.reason, ) } return source.INVALID_DIAGNOSTIC } add_unsupported_type_diagnostic :: proc( checker: ^Checker, span: source.Span, value: types.Type, depth := 0, ) -> source.Diagnostic_Id { if depth > 64 { return source.INVALID_DIAGNOSTIC } item, ok := types.node(&checker.module.types, value) if !ok { return source.INVALID_DIAGNOSTIC } if item.kind == .Alias { return add_unsupported_diagnostic(checker, span, ast.Package_Id(item.pkg), symbol.Id(item.name)) } if types.is_valid(item.child) { return add_unsupported_type_diagnostic(checker, span, item.child, depth+1) } if types.is_valid(item.extra) { return add_unsupported_type_diagnostic(checker, span, item.extra, depth+1) } return source.INVALID_DIAGNOSTIC } function_signatures_equal :: proc(left, right: ast.Function) -> bool { if left.result != right.result || left.error != right.error || left.variadic != right.variadic || len(left.params) != len(right.params) { return false } for param, index in left.params { if param.type != right.params[index].type || param.comptime_value != right.params[index].comptime_value { return false } } return true } valid_call_arity :: proc(function: ast.Function, count: int) -> bool { return count >= len(function.params) if function.variadic else count == len(function.params) } Call_Argument_Mode :: enum u8 { Invalid, Explicit, Inferred, } comptime_prefix_count :: proc(function: ast.Function) -> (int, bool) { count := 0 for param in function.params { if param.comptime_value { count += 1 continue } break } for param in function.params[count:] { if param.comptime_value { return count, false } } return count, count > 0 } call_argument_mode :: proc(function: ast.Function, count: int) -> (Call_Argument_Mode, int) { if valid_call_arity(function, count) { return .Explicit, 0 } prefix, inferable := comptime_prefix_count(function) if inferable && !function.c_abi && !function.variadic && count == len(function.params)-prefix { return .Inferred, prefix } return .Invalid, prefix } call_param_index :: proc(mode: Call_Argument_Mode, prefix, source_index: int) -> int { return source_index+prefix if mode == .Inferred else source_index } next_runtime_call_arg :: proc(function: ast.Function, mode: Call_Argument_Mode, prefix, start: int, source_count: int) -> int { index := start for index < source_count { param_index := call_param_index(mode, prefix, index) if param_index >= len(function.params) || !function.params[param_index].comptime_value { break } index += 1 } return index } comptime_binding_index :: proc(function: ast.Function, prefix: int, name: symbol.Id) -> (int, bool) { for param, index in function.params[:prefix] { if param.name == name { return index, true } } return -1, false } bind_inferred_comptime :: proc( checker: ^Checker, function: ast.Function, prefix: int, values: []Comptime_Value, bound: []bool, name: symbol.Id, candidate: Comptime_Value, span: source.Span, diagnose: bool, ) -> bool { index, ok := comptime_binding_index(function, prefix, name) if !ok { return false } value := candidate value.name = name if !bound[index] { values[index] = value bound[index] = true return true } existing := values[index] matches := existing.kind == value.kind if matches { if existing.kind == .Type { matches = types.equal(types.resolve_alias(existing.type, &checker.module.types), types.resolve_alias(value.type, &checker.module.types)) } else { matches = existing.value == value.value && types.equal(existing.type, value.type) } } if !matches && diagnose { left := type_label(checker, existing.type) if existing.kind == .Type else fmt.aprintf("%d", existing.value, allocator=checker.allocator) right := type_label(checker, value.type) if value.kind == .Type else fmt.aprintf("%d", value.value, allocator=checker.allocator) source.addf(checker.diagnostics, span, "conflicting inference for comptime parameter '%s': %s and %s", symbol_text(checker, name), left, right) if existing.kind != .Type { delete(left, checker.allocator) } if value.kind != .Type { delete(right, checker.allocator) } } return matches } type_pattern_mentions_comptime :: proc( checker: ^Checker, function: ast.Function, prefix: int, pattern: types.Type, depth := 0, ) -> bool { if depth > 64 { return false } item, ok := types.node(&checker.module.types, pattern) if !ok { return false } if item.qualifier == 0 && item.name != 0 { if _, found := comptime_binding_index(function, prefix, symbol.Id(item.name)); found { return true } } if item.kind == .Array && item.unresolved_count { expr_id := ast.Expr_Id(item.count_expr) if expr_id != ast.INVALID_EXPR && int(expr_id) < len(checker.ast_module.exprs) { expr := checker.ast_module.exprs[expr_id] if expr.kind == .Name && !symbol.is_valid(expr.qualifier) { if _, found := comptime_binding_index(function, prefix, expr.name); found { return true } } } } if item.kind == .Type_Call { expr_id := ast.Expr_Id(item.count_expr) if expr_id != ast.INVALID_EXPR && int(expr_id) < len(checker.ast_module.exprs) { for arg_id in checker.ast_module.exprs[expr_id].args { if arg_id == ast.INVALID_EXPR || int(arg_id) >= len(checker.ast_module.exprs) { continue } arg := checker.ast_module.exprs[arg_id] if arg.kind == .Name && !symbol.is_valid(arg.qualifier) { if _, found := comptime_binding_index(function, prefix, arg.name); found { return true } } } } } if types.is_valid(item.child) && type_pattern_mentions_comptime(checker, function, prefix, item.child, depth+1) { return true } if types.is_valid(item.extra) && type_pattern_mentions_comptime(checker, function, prefix, item.extra, depth+1) { return true } if item.kind == .Function { for field in types.params_for(&checker.module.types, pattern) { if type_pattern_mentions_comptime(checker, function, prefix, field.type, depth+1) { return true } } } return false } match_inferred_type_pattern :: proc( checker: ^Checker, function: ast.Function, prefix: int, pattern, actual: types.Type, values: []Comptime_Value, bound: []bool, span: source.Span, diagnose: bool, depth := 0, ) -> bool { if depth > 64 || !types.is_valid(actual) { return false } store := &checker.module.types actual_type := types.resolve_alias(actual, store) pattern_item, pattern_ok := types.node(store, pattern) if pattern_ok && pattern_item.qualifier == 0 && pattern_item.name != 0 { name := symbol.Id(pattern_item.name) if index, is_binding := comptime_binding_index(function, prefix, name); is_binding && is_comptime_type_param(checker, function.params[index]) { return bind_inferred_comptime( checker, function, prefix, values, bound, name, Comptime_Value{type=actual_type, kind=.Type}, span, diagnose, ) } } if !pattern_ok { return types.equal(pattern, actual_type) } if pattern_item.kind == .Type_Call { expr_id := ast.Expr_Id(pattern_item.count_expr) if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) { return false } expr := checker.ast_module.exprs[expr_id] target_pkg, available := expr_package(checker, expr, function.pkg, function.file, true) if !available { return false } template := find_template(checker, expr.name, target_pkg, expr_lookup_file(expr, function.file)) origin: Type_Factory_Origin found := false for candidate in checker.type_factory_origins { if candidate.template == template && types.equal(candidate.result, actual_type) { origin = candidate found = true break } } if !found || len(expr.args) != len(origin.values) { return false } matched := true for arg_id, index in expr.args { if arg_id == ast.INVALID_EXPR || int(arg_id) >= len(checker.ast_module.exprs) { matched = false continue } arg := checker.ast_module.exprs[arg_id] if arg.kind == .Name && !symbol.is_valid(arg.qualifier) { if _, is_binding := comptime_binding_index(function, prefix, arg.name); is_binding { matched = bind_inferred_comptime( checker, function, prefix, values, bound, arg.name, origin.values[index], span, diagnose, ) && matched } } } return matched } actual_item, actual_ok := types.node(store, actual_type) if !actual_ok || pattern_item.kind != actual_item.kind { resolved := type_from_syntax(checker, pattern, function.pkg, function.file) return types.is_valid(resolved) && types.equal(types.resolve_alias(resolved, store), actual_type) } if pattern_item.many != actual_item.many || pattern_item.has_sentinel != actual_item.has_sentinel || pattern_item.has_sentinel && pattern_item.sentinel != actual_item.sentinel { return false } matched := true if pattern_item.kind == .Array { if pattern_item.unresolved_count { count_expr := ast.Expr_Id(pattern_item.count_expr) if count_expr != ast.INVALID_EXPR && int(count_expr) < len(checker.ast_module.exprs) { expr := checker.ast_module.exprs[count_expr] if expr.kind == .Name && !symbol.is_valid(expr.qualifier) { if binding_index, is_binding := comptime_binding_index(function, prefix, expr.name); is_binding && !is_comptime_type_param(checker, function.params[binding_index]) { matched = bind_inferred_comptime( checker, function, prefix, values, bound, expr.name, Comptime_Value{type=values[binding_index].type, value=i128(actual_item.count), kind=.Integer}, span, diagnose, ) && matched } } } } else if pattern_item.count != actual_item.count { matched = false } } if types.is_valid(pattern_item.child) { matched = match_inferred_type_pattern( checker, function, prefix, pattern_item.child, actual_item.child, values, bound, span, diagnose, depth+1, ) && matched } if types.is_valid(pattern_item.extra) { matched = match_inferred_type_pattern( checker, function, prefix, pattern_item.extra, actual_item.extra, values, bound, span, diagnose, depth+1, ) && matched } if pattern_item.kind == .Function { pattern_params := types.params_for(store, pattern) actual_params := types.params_for(store, actual_type) if len(pattern_params) != len(actual_params) || pattern_item.c_abi != actual_item.c_abi || pattern_item.variadic != actual_item.variadic { return false } for field, index in pattern_params { matched = match_inferred_type_pattern( checker, function, prefix, field.type, actual_params[index].type, values, bound, span, diagnose, depth+1, ) && matched } } return matched } infer_call_comptime_values :: proc( checker: ^Checker, function: ast.Function, prefix: int, args: []ast.Expr_Id, actual_args: []types.Type, expected: types.Type, pkg: ast.Package_Id, file: ast.File_Id, diagnose := false, ) -> ([]Comptime_Value, bool) { values := make([]Comptime_Value, prefix, checker.allocator) bound := make([]bool, prefix, checker.allocator) defer delete(bound, checker.allocator) for param, index in function.params[:prefix] { values[index].name = param.name if is_comptime_type_param(checker, param) { values[index].kind = .Type } else { values[index].kind = .Integer values[index].type = type_from_syntax(checker, param.type, function.pkg, function.file) } } matched := true if is_runtime_type(checker, expected) { if types.is_valid(function.error) && types.kind(expected, &checker.module.types) == .Fallible { matched = match_inferred_type_pattern( checker, function, prefix, function.result, types.fallible_success(expected, &checker.module.types), values, bound, source.Span{}, diagnose, ) && matched matched = match_inferred_type_pattern( checker, function, prefix, function.error, types.fallible_error(expected, &checker.module.types), values, bound, source.Span{}, diagnose, ) && matched } else if type_pattern_mentions_comptime(checker, function, prefix, function.result) { matched = match_inferred_type_pattern( checker, function, prefix, function.result, expected, values, bound, source.Span{}, diagnose, ) && matched } } // Concrete arguments bind first. Numeric constants are contextual and therefore // only contribute their default type after stronger evidence has had a chance. weak_passes := [2]bool{false, true} for weak in weak_passes { for arg_id, source_index in args { param_index := prefix+source_index if param_index >= len(function.params) || param_index >= len(actual_args) { continue } is_weak := is_numeric_constant_expr(checker, arg_id) if is_weak != weak { continue } if !type_pattern_mentions_comptime(checker, function, prefix, function.params[param_index].type) { continue } if weak { if item, ok := types.node(&checker.module.types, function.params[param_index].type); ok && item.qualifier == 0 && item.name != 0 { if binding_index, is_binding := comptime_binding_index(function, prefix, symbol.Id(item.name)); is_binding && bound[binding_index] { continue } } } matched = match_inferred_type_pattern( checker, function, prefix, function.params[param_index].type, actual_args[param_index], values, bound, checker.ast_module.exprs[arg_id].span, diagnose, ) && matched } } for param, index in function.params[:prefix] { if bound[index] { continue } matched = false if diagnose { source.addf( checker.diagnostics, param.span, "cannot infer comptime parameter '%s'; pass it explicitly", symbol_text(checker, param.name), ) } } if !matched { delete(values, checker.allocator) return nil, false } return values, true } call_arg_expected :: proc(checker: ^Checker, function: ast.Function, index: int) -> types.Type { if index < 0 || index >= len(function.params) { return types.INVALID } if is_comptime_type_param(checker, function.params[index]) { return types.INVALID } for param in function.params { if !param.comptime_value { continue } if _, ok := current_comptime_value(checker, param.name); !ok { return types.INVALID } } declared := type_from_syntax(checker, function.params[index].type, function.pkg, function.file) // A `float` param defaults to f64 so an integer-literal argument builds as a // float constant (e.g. `f(3)` -> 3.0), mirroring `pi float = 3` for locals. // `int`/`range` constraints have no single default and keep building naturally. if declared == types.FLOAT { return types.F64 } return declared } resolve_type_argument :: proc( checker: ^Checker, expr_id: ast.Expr_Id, pkg: ast.Package_Id, file: ast.File_Id, ) -> (types.Type, bool) { if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) { return types.INVALID, false } expr := checker.ast_module.exprs[expr_id] #partial switch expr.kind { case .Type: resolved := type_from_syntax(checker, expr.type, pkg, file) return resolved, types.is_valid(resolved) case .Name: if !symbol.is_valid(expr.qualifier) { if actual, ok := current_comptime_type(checker, expr.name); ok { return actual, true } } target_pkg, available := expr_package(checker, expr, pkg, file) if !available { return types.INVALID, false } value := types.find_named(&checker.module.types, u32(target_pkg), u32(expr.name), file=u32(expr_lookup_file(expr, file))) value = types.resolve_alias(value, &checker.module.types) return value, types.is_valid(value) case .Call: value := resolve_type_factory_call(checker, expr_id, pkg, file) return value, types.is_valid(value) } return types.INVALID, false } clone_comptime_values :: proc(values: []Comptime_Value, allocator: mem.Allocator) -> []Comptime_Value { result := make([]Comptime_Value, len(values), allocator) copy(result, values) return result } resolve_generated_struct_type :: proc(checker: ^Checker, expr_id: ast.Expr_Id, pkg: ast.Package_Id, file: ast.File_Id) -> types.Type { for entry in checker.generated_types { if entry.expr == expr_id && comptime_values_equal(entry.values, checker.current_comptime_values) { return entry.result } } if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) { return types.INVALID } expr := checker.ast_module.exprs[expr_id] field_start := int(u32(expr.integer>>32)) field_count := int(u32(expr.integer)) if field_start < 0 || field_count < 0 || field_start+field_count > len(checker.ast_module.type_fields) { return types.INVALID } template_fields := checker.ast_module.type_fields[field_start:field_start+field_count] fields := make([]types.Field, len(template_fields), checker.allocator) defer delete(fields, checker.allocator) for field, index in template_fields { resolved := type_from_syntax(checker, field.type, pkg, file) if !is_runtime_type(checker, resolved) || types.is_void(resolved) { source.addf(checker.diagnostics, expr.span, "anonymous struct field '%s' requires a concrete runtime type, got %s", symbol_text(checker, symbol.Id(field.name)), type_label(checker, resolved)) return types.INVALID } fields[index] = types.Field{name=field.name, type=resolved} } result := types.struct_generated(&checker.module.types, fields) append(&checker.generated_types, Generated_Type_Entry{ expr=expr_id, values=clone_comptime_values(checker.current_comptime_values, checker.allocator), result=result, }) return result } resolve_type_factory_call :: proc(checker: ^Checker, expr_id: ast.Expr_Id, pkg: ast.Package_Id, file: ast.File_Id) -> types.Type { if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) { return types.INVALID } expr := checker.ast_module.exprs[expr_id] if expr.kind != .Call || expr.left != ast.INVALID_EXPR { source.add(checker.diagnostics, expr.span, "type position requires a direct type-factory call") return types.INVALID } target_pkg, available := expr_package(checker, expr, pkg, file, true) if !available { return types.INVALID } template := find_template(checker, expr.name, target_pkg, expr_lookup_file(expr, file)) if template == ast.INVALID_FUNCTION || int(template) >= len(checker.ast_module.functions) { source.addf(checker.diagnostics, expr.span, "unknown type factory '%s'", symbol_text(checker, expr.name)) return types.INVALID } function := checker.ast_module.functions[template] if !is_type_metatype_syntax(checker, function.result) || types.is_valid(function.error) { source.addf(checker.diagnostics, expr.span, "function '%s' does not return a type", symbol_text(checker, expr.name)) return types.INVALID } for param in function.params { if !param.comptime_value { source.addf(checker.diagnostics, param.span, "type-factory parameter '%s' must be comptime", symbol_text(checker, param.name)) return types.INVALID } } if !valid_call_arity(function, len(expr.args)) { source.addf(checker.diagnostics, expr.span, "type factory '%s' expects %d arguments, got %d", symbol_text(checker, expr.name), len(function.params), len(expr.args)) return types.INVALID } values, ok := collect_comptime_values(checker, function, expr.args, pkg, file, true, checker.current_comptime_values) defer delete(values, checker.allocator) if !ok { return types.INVALID } // A generic function's declaration is validated before it has a specialization. // Leave calls containing its unresolved type parameters pending until then. for value in values { if value.kind != .Type { continue } if item, item_ok := types.node(&checker.module.types, value.type); item_ok && item.kind == .Named && !item.declared { return types.INVALID } } for &entry in checker.type_factories { if entry.template != template || !comptime_values_equal(entry.values, values) { continue } if entry.resolving { source.addf(checker.diagnostics, expr.span, "recursive type-factory specialization of '%s'", symbol_text(checker, expr.name)) return types.INVALID } return entry.result } entry_index := len(checker.type_factories) append(&checker.type_factories, Type_Factory_Entry{ template=template, values=clone_comptime_values(values, checker.allocator), result=types.INVALID, resolving=true, }) state := ct_state_make(checker, pkg, file) value, flow, eval_ok := ct_eval_call_expr(&state, expr, function.result, 0) result := types.INVALID if eval_ok && flow.kind == .Normal && value != INVALID_CT_VALUE && int(value) < len(state.values) && state.values[value].kind == .Type { result = types.Type(state.values[value].index) } else if state.diagnostic == source.INVALID_DIAGNOSTIC { source.addf(checker.diagnostics, expr.span, "type factory '%s' did not return a type", symbol_text(checker, expr.name)) } ct_state_destroy(&state) checker.type_factories[entry_index].result = result checker.type_factories[entry_index].resolving = false if types.is_valid(result) { generated := false for entry in checker.generated_types { if types.equal(entry.result, result) { generated = true break } } if generated { has_origin := false for origin in checker.type_factory_origins { if types.equal(origin.result, result) { has_origin = true break } } if !has_origin { append(&checker.type_factory_origins, Type_Factory_Origin{ result=result, template=template, values=clone_comptime_values(values, checker.allocator), }) } } } return result } collect_comptime_values :: proc( checker: ^Checker, function: ast.Function, args: []ast.Expr_Id, pkg: ast.Package_Id, file: ast.File_Id, diagnose := false, extra_values: []Comptime_Value = nil, ) -> ([]Comptime_Value, bool) { if !function_has_comptime_params(function) { return nil, true } values: [dynamic]Comptime_Value values.allocator = checker.allocator ok := true for param, index in function.params { if !param.comptime_value { continue } span := param.span if index < len(args) && args[index] != ast.INVALID_EXPR && int(args[index]) < len(checker.ast_module.exprs) { span = checker.ast_module.exprs[args[index]].span } if is_comptime_type_param(checker, param) { actual, actual_ok := types.INVALID, false if index < len(args) { actual, actual_ok = resolve_type_argument(checker, args[index], pkg, file) } if !actual_ok { if diagnose { source.addf( checker.diagnostics, span, "argument for comptime type parameter '%s' must be a type", symbol_text(checker, param.name), ) } ok = false continue } append(&values, Comptime_Value{name=param.name, type=actual, kind=.Type}) continue } declared := type_from_syntax(checker, param.type, function.pkg, function.file) if !types.is_concrete_integer(declared) { if diagnose { source.addf( checker.diagnostics, param.span, "comptime parameter '%s' requires a concrete integer type", symbol_text(checker, param.name), ) } ok = false continue } constant := Constant{kind = .Not_Constant} if index < len(args) { constant = eval_integer_constant_in_context(checker, args[index], pkg, file, values=extra_values) } if constant.kind != .Value { if diagnose { source.addf( checker.diagnostics, span, "argument for comptime parameter '%s' must be a compile-time integer expression", symbol_text(checker, param.name), ) } ok = false continue } if !fits_integer_type(constant.value, declared, checker.target) { if diagnose { source.addf( checker.diagnostics, span, "integer constant %d does not fit in %s", constant.value, types.name(declared), ) } ok = false continue } append(&values, Comptime_Value{name=param.name, type=declared, value=constant.value}) } if !ok { delete(values) return nil, false } return values[:], true } callable_arg_expected :: proc(function_type: types.Type, function_item: types.Node, store: ^types.Store, index: int) -> types.Type { if index < 0 || index >= int(function_item.field_count) { return types.INVALID } params := types.params_for(store, function_type) if index >= len(params) { return types.INVALID } return params[index].type } valid_callable_arity :: proc(function_item: types.Node, count: int) -> bool { return count >= int(function_item.field_count) if function_item.variadic else count == int(function_item.field_count) } function_value_signature :: proc( checker: ^Checker, template: ast.Function_Id, ) -> (params: []types.Type, result: types.Type, ok: bool) { if template == ast.INVALID_FUNCTION || int(template) >= len(checker.ast_module.functions) { return nil, types.INVALID, false } function := checker.ast_module.functions[template] if function_has_comptime_params(function) { return nil, types.INVALID, false } if function.c_abi && types.is_valid(function.error) { return nil, types.INVALID, false } if !function.c_abi && (!function.has_body || function.variadic) { return nil, types.INVALID, false } result = function_channel_type(checker, function) if !types.is_void(result) && !is_runtime_type(checker, result) { return nil, types.INVALID, false } params = make([]types.Type, len(function.params), checker.allocator) for param, index in function.params { param_type := type_from_syntax(checker, param.type, function.pkg, function.file) if !is_runtime_type(checker, param_type) { delete(params, checker.allocator) return nil, types.INVALID, false } params[index] = param_type } return params, result, true } function_pointer_type_for_template :: proc( checker: ^Checker, template: ast.Function_Id, demanded: ^[dynamic]Spec_Id = nil, demand_spec := true, ) -> (types.Type, Spec_Id, bool) { params, result, ok := function_value_signature(checker, template) if !ok { return types.INVALID, INVALID_SPEC, false } defer delete(params, checker.allocator) function := checker.ast_module.functions[template] function_type := types.function(&checker.module.types, params, result, function.c_abi, function.variadic) pointer_type := types.pointer(&checker.module.types, function_type, false, false) spec := INVALID_SPEC if demanded == nil { if demand_spec { spec = ensure_spec(checker, template, params) } else { spec = find_spec(checker, template, params) } } else { spec = find_spec(checker, template, params) mark_spec_demanded(checker, spec, demanded) } return pointer_type, spec, spec != INVALID_SPEC || !demand_spec } contains_name :: proc(names: []symbol.Id, name: symbol.Id) -> bool { for existing in names { if existing == name { return true } } return false } mark_expr_imports_used :: proc(checker: ^Checker, expr_id: ast.Expr_Id, file: ast.File_Id) { stack := checker.ast_expr_stack clear_dynamic_array(&stack) defer { clear_dynamic_array(&stack) checker.ast_expr_stack = stack } append(&stack, expr_id) for len(stack) > 0 { id := pop(&stack) if id == ast.INVALID_EXPR || int(id) >= len(checker.ast_module.exprs) { continue } expr := checker.ast_module.exprs[id] if (expr.kind == .Name || expr.kind == .Call) && symbol.is_valid(expr.qualifier) { _ = find_import(checker, file, expr.qualifier, true) } switch expr.kind { case .Call: append(&stack, ..expr.args) if expr.left != ast.INVALID_EXPR { append(&stack, expr.left) } case .Array, .Struct_Literal, .Slice: append(&stack, ..expr.args) if expr.left != ast.INVALID_EXPR { append(&stack, expr.left) } case .Negate, .Not, .Address, .Deref, .Field, .Unwrap, .Try, .Keyed, .Enum_Literal, .Cast: append(&stack, expr.left) case .Comptime: if expr.left != ast.INVALID_EXPR { append(&stack, expr.left) } mark_block_imports_used(checker, expr.body, file) case .Catch: append(&stack, expr.left) if expr.right != ast.INVALID_EXPR { append(&stack, expr.right) } mark_block_imports_used(checker, expr.body, file) case .Function_Literal: function_id := ast.Function_Id(u32(expr.integer)) if function_id != ast.INVALID_FUNCTION && int(function_id) < len(checker.ast_module.functions) { function := checker.ast_module.functions[function_id] mark_block_imports_used(checker, function.body, function.file) } case .Add, .Sub, .Mul, .Div, .Index, .Orelse, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range: append(&stack, expr.left, expr.right) case .Invalid, .Integer, .Float, .String, .Bool, .None, .Undefined, .Type, .Name, .Anonymous_Struct_Type: } } } mark_block_imports_used :: proc(checker: ^Checker, statements: []ast.Stmt_Id, file: ast.File_Id) { for statement_id in statements { statement := checker.ast_module.statements[statement_id] switch statement.kind { case .Declaration, .Assignment, .Return, .Expression, .Yield: mark_expr_imports_used(checker, statement.expr, file) if statement.target != ast.INVALID_EXPR { mark_expr_imports_used(checker, statement.target, file) } // A value-block declaration/assignment carries its block in `body`. mark_block_imports_used(checker, statement.body, file) case .If: mark_expr_imports_used(checker, statement.expr, file) if statement.guard != ast.INVALID_EXPR { mark_expr_imports_used(checker, statement.guard, file) } mark_block_imports_used(checker, statement.body, file) mark_block_imports_used(checker, statement.else_body, file) case .While: mark_expr_imports_used(checker, statement.expr, file) mark_block_imports_used(checker, statement.body, file) if statement.update != ast.INVALID_STMT { update := [1]ast.Stmt_Id{statement.update} mark_block_imports_used(checker, update[:], file) } case .For: mark_expr_imports_used(checker, statement.expr, file) mark_block_imports_used(checker, statement.body, file) case .Block: mark_block_imports_used(checker, statement.body, file) case .Defer: deferred := [1]ast.Stmt_Id{statement.update} mark_block_imports_used(checker, deferred[:], file) case .Match, .Match_Arm: // `Match` carries the subject in `expr` and arms in `body`; each `Match_Arm` // carries its patterns in `patterns` and the arm body in `body`. mark_expr_imports_used(checker, statement.expr, file) for pattern in statement.patterns { mark_expr_imports_used(checker, pattern, file) } mark_block_imports_used(checker, statement.body, file) case .Break, .Continue: case .Invalid: } } } validate_external_globals :: proc(checker: ^Checker) { for global, global_index in checker.ast_module.globals { checker.external_global_canonical[global_index] = ast.global_id(global_index) if !global.external { continue } switch global.link_name { case "main": checker.external_global_diagnostics[global_index] = source.add( checker.diagnostics, global.span, "external C variable 'main' conflicts with the program entry point", ) case "write": checker.external_global_diagnostics[global_index] = source.add( checker.diagnostics, global.span, "external C variable 'write' conflicts with the compiler runtime", ) } for previous, previous_index in checker.ast_module.globals[:global_index] { if !previous.external || previous.link_name != global.link_name { continue } canonical := checker.external_global_canonical[previous_index] if canonical == ast.INVALID_GLOBAL { canonical = ast.global_id(previous_index) } canonical_index := int(canonical) if canonical_index < 0 || canonical_index >= len(checker.ast_module.globals) { canonical = ast.global_id(previous_index) canonical_index = previous_index } checker.external_global_canonical[global_index] = canonical canonical_global := checker.ast_module.globals[canonical_index] canonical_type := checker.global_types[canonical_index] if !types.equal(checker.global_types[global_index], canonical_type) || global.writable != canonical_global.writable { checker.external_global_diagnostics[global_index] = source.addf( checker.diagnostics, global.span, "conflicting external C variable declarations for '%s'", global.link_name, ) } checker.global_types[global_index] = canonical_type break } for function in checker.ast_module.functions { if !function.c_abi || function.has_body || len(function.unsupported_reason) > 0 || symbol_text(checker, function.name) != global.link_name { continue } if checker.external_global_diagnostics[global_index] == source.INVALID_DIAGNOSTIC { checker.external_global_diagnostics[global_index] = source.addf( checker.diagnostics, global.span, "external C variable '%s' conflicts with a C function declaration", global.link_name, ) } break } } } validate_declarations :: proc(checker: ^Checker) { for function, function_id in checker.ast_module.functions { if len(function.unsupported_reason) > 0 { continue } has_comptime := function_has_comptime_params(function) locals: [dynamic]symbol.Id locals.allocator = checker.allocator seen_runtime_param := false comptime_order_reported := false for param in function.params { if param.comptime_value { if seen_runtime_param && !comptime_order_reported { checker.template_diagnostics[function_id] = source.add( checker.diagnostics, param.span, "comptime parameters must form a leading parameter prefix", ) comptime_order_reported = true } } else { seen_runtime_param = true } param_type := types.INVALID if param.comptime_value || !has_comptime { param_type = type_from_syntax(checker, param.type, function.pkg, function.file) } if param.comptime_value { if function.c_abi { checker.template_diagnostics[function_id] = source.add( checker.diagnostics, param.span, "comptime parameters require 'func', not 'c_func'", ) } if !is_type_metatype_syntax(checker, param.type) && !types.is_concrete_integer(param_type) { checker.template_diagnostics[function_id] = source.addf( checker.diagnostics, param.span, "comptime parameter '%s' requires a concrete integer type", symbol_text(checker, param.name), ) } } else if !has_comptime { if diagnostic := add_unsupported_type_diagnostic(checker, param.span, param_type); diagnostic != source.INVALID_DIAGNOSTIC { checker.template_diagnostics[function_id] = diagnostic continue } } if param.type == types.VOID { source.add( checker.diagnostics, param.span, "void is only valid as a function result type", ) } if param.name != checker.sink_symbol && contains_name(locals[:], param.name) { source.addf( checker.diagnostics, param.span, "duplicate parameter '%s'", symbol_text(checker, param.name), ) } else { _ = add_shadow_diagnostic( checker, param.span, param.name, "parameter", function.pkg, function.file, ) } append(&locals, param.name) if !has_comptime && types.contains_c_struct_by_value(param_type, &checker.module.types) { checker.template_diagnostics[function_id] = source.addf( checker.diagnostics, param.span, "C records cannot be passed by value to '%s'", symbol_text(checker, function.name), ) } } if !has_comptime { result_type := type_from_syntax(checker, function.result, function.pkg, function.file) if diagnostic := add_unsupported_type_diagnostic(checker, function.span, result_type); diagnostic != source.INVALID_DIAGNOSTIC { checker.template_diagnostics[function_id] = diagnostic } if types.contains_c_struct_by_value(result_type, &checker.module.types) { checker.template_diagnostics[function_id] = source.addf( checker.diagnostics, function.span, "C records cannot be returned by value from '%s'", symbol_text(checker, function.name), ) } } if types.is_valid(function.error) { error_type := type_from_syntax(checker, function.error, function.pkg, function.file) error_sum := types.is_enum(error_type, &checker.module.types) || types.is_tagged_union(error_type, &checker.module.types) if function.c_abi { checker.template_diagnostics[function_id] = source.add( checker.diagnostics, function.span, "fallible functions must use 'func', not 'c_func'", ) } else if !error_sum { checker.template_diagnostics[function_id] = source.add( checker.diagnostics, function.span, "fallible function error type must be a native enum or tagged union", ) } } if !function.has_body && !function.c_abi { checker.template_diagnostics[function_id] = source.addf( checker.diagnostics, function.span, "bodyless function '%s' must use 'c_func'", symbol_text(checker, function.name), ) } if function.variadic && (!function.c_abi || function.has_body) { checker.template_diagnostics[function_id] = source.addf( checker.diagnostics, function.span, "variadic function '%s' must be a bodyless 'c_func' declaration", symbol_text(checker, function.name), ) } if !function.has_body && function.c_abi { for param in function.params { param_type := type_from_syntax(checker, param.type, function.pkg, function.file) if add_unsupported_type_diagnostic(checker, param.span, param_type) != source.INVALID_DIAGNOSTIC { continue } if types.contains_c_struct_by_value(param_type, &checker.module.types) { continue } if !types.is_c_signature_type(param_type, &checker.module.types) { checker.template_diagnostics[function_id] = source.addf( checker.diagnostics, param.span, "foreign function '%s' requires concrete parameter types", symbol_text(checker, function.name), ) } } result := type_from_syntax(checker, function.result, function.pkg, function.file) if add_unsupported_type_diagnostic(checker, function.span, result) == source.INVALID_DIAGNOSTIC && !types.contains_c_struct_by_value(result, &checker.module.types) && !types.is_c_signature_type(result, &checker.module.types, true) { checker.template_diagnostics[function_id] = source.addf( checker.diagnostics, function.span, "foreign function '%s' requires a concrete or void result type", symbol_text(checker, function.name), ) } if function.pkg == 0 && function.name == checker.main_symbol { checker.template_diagnostics[function_id] = source.add( checker.diagnostics, function.span, "main must have a body", ) } } mark_block_imports_used(checker, function.body, function.file) delete(locals) } for function, function_id in checker.ast_module.functions { if function.has_body || !function.c_abi { continue } for other, other_id in checker.ast_module.functions { if other_id == function_id || other.has_body || !other.c_abi || other.name != function.name { continue } if function.imported && other.imported && function_signatures_equal(function, other) { continue } checker.template_diagnostics[function_id] = source.addf( checker.diagnostics, function.span, "duplicate foreign symbol '%s'", symbol_text(checker, function.name), ) break } } } validate_type_nodes :: proc(checker: ^Checker) { for item, index in checker.module.types.nodes { id := types.DYNAMIC_START+types.Type(index) if item.kind == .Distinct && (!item.declared || !types.is_runtime_value(item.child, &checker.module.types)) { source.addf( checker.diagnostics, source.Span{}, "distinct type '%s' requires a concrete runtime backing type", symbol_text(checker, symbol.Id(item.name)), ) } if item.kind == .Enum { if !item.declared || !types.is_concrete_integer(item.child) { source.addf( checker.diagnostics, source.Span{}, "enum type '%s' requires a concrete integer backing type", symbol_text(checker, symbol.Id(item.name)), ) } else { for member in types.enum_members_for(&checker.module.types, id) { if !fits_integer_type(member.value, item.child, checker.target) { source.addf( checker.diagnostics, source.Span{}, "enum value %d does not fit in %s", member.value, types.name(item.child), ) } } } } if item.has_sentinel { value := i128(item.sentinel) if types.is_signed(item.child, checker.target) { value = i128(i64(item.sentinel)) } if !types.is_concrete_integer(item.child) || !fits_integer_type(value, item.child, checker.target) { source.addf( checker.diagnostics, source.Span{}, "sentinel value does not fit array, slice, or pointer element type %s", types.name(item.child), ) } } if item.kind == .Struct || item.kind == .Union { if item.c_layout && !item.opaque && item.field_count == 0 { source.add( checker.diagnostics, source.Span{}, "c_struct definitions require at least one field", ) } // A tagged union may carry `void`-payload variants (`.quit void`): the // variant has no runtime value, only a tag. Allowed only here, not for // structs, untagged unions, or c_structs. tagged_union := item.kind == .Union && types.is_enum(item.child, &checker.module.types) for field in types.fields_for(&checker.module.types, id) { if tagged_union && types.is_void(field.type) { // void variant: no payload to validate. } else if !types.is_runtime_value(field.type, &checker.module.types) { source.add( checker.diagnostics, source.Span{}, "record fields must have runtime value types", ) } else if item.c_layout && !types.is_c_record_field_type(field.type, &checker.module.types) { source.add( checker.diagnostics, source.Span{}, "c_struct fields must have C-layout-compatible types", ) } } // A tagged union stores a hidden runtime tag enum keyed by global variant IDs. // An explicit `union(Enum)` keeps that declared enum only for validation. if item.kind == .Union && types.is_valid(item.child) { declared_tag := types.union_declared_tag_enum(id, &checker.module.types) if !types.is_enum(declared_tag, &checker.module.types) { source.add(checker.diagnostics, source.Span{}, "a tagged union's tag must be an enum") } else { for field in types.fields_for(&checker.module.types, id) { if _, ok := find_enum_member(checker, declared_tag, symbol.Id(field.name)); !ok { source.addf( checker.diagnostics, source.Span{}, "union variant '%s' is not a member of the tag enum", symbol_text(checker, symbol.Id(field.name)), ) } } } } } if item.kind == .Function { if item.c_abi { if types.kind(item.child, &checker.module.types) == .Fallible { source.add(checker.diagnostics, source.Span{}, "c_func pointer results cannot be fallible") } for param in types.params_for(&checker.module.types, id) { if types.is_void(param.type) || !types.is_c_signature_type(param.type, &checker.module.types) { source.add(checker.diagnostics, source.Span{}, "c_func pointer parameters must be concrete C signature types") } } if !types.is_c_signature_type(item.child, &checker.module.types, true) { source.add(checker.diagnostics, source.Span{}, "c_func pointer results must be concrete C signature types or void") } } else { if item.variadic { source.add(checker.diagnostics, source.Span{}, "native function pointer types do not support variadic parameters") } for param in types.params_for(&checker.module.types, id) { if types.is_void(param.type) || !is_runtime_type(checker, param.type) { source.add(checker.diagnostics, source.Span{}, "native function pointer parameters must be concrete runtime types") } } if !types.is_void(item.child) && !is_runtime_type(checker, item.child) { source.add(checker.diagnostics, source.Span{}, "native function pointer results must be concrete runtime types or void") } } } } } find_infer_local :: proc(locals: []Infer_Local, name: symbol.Id) -> types.Type { if index, ok := find_infer_local_index(locals, name); ok { return locals[index].type } return types.INVALID } find_infer_local_index :: proc(locals: []Infer_Local, name: symbol.Id) -> (int, bool) { for index := len(locals) - 1; index >= 0; index -= 1 { if locals[index].name == name { return index, true } } return -1, false } find_spec :: proc( checker: ^Checker, template: ast.Function_Id, actual_args: []types.Type, comptime_values: []Comptime_Value = nil, ) -> Spec_Id { function := checker.ast_module.functions[template] for spec, index in checker.specs { if spec.template != template || len(spec.args) != runtime_param_count(function) || !comptime_values_equal(spec.comptime_values, comptime_values) { continue } matches := true runtime_index := 0 previous_comptime := checker.current_comptime_values checker.current_comptime_values = comptime_values for param, param_index in function.params { if param.comptime_value { continue } actual := types.INVALID if param_index < len(actual_args) { actual = actual_args[param_index] } if runtime_index >= len(spec.args) || !types.equal(spec.args[runtime_index], specialized_param_type(checker, param.type, actual, function.pkg, function.file)) { matches = false break } runtime_index += 1 } checker.current_comptime_values = previous_comptime if matches { return spec_id(index) } } return INVALID_SPEC } // specialized_param_type maps a parameter's declared type to its monomorphized // type for a given actual argument. A constraint param (`int`/`float`/`range`) // resolves to the actual's family member (INVALID if out of family), so a call // passing an out-of-family argument fails to specialize and is rejected. specialized_param_type :: proc( checker: ^Checker, syntax: ast.Type_Syntax, actual: types.Type, pkg: ast.Package_Id, file: ast.File_Id, ) -> types.Type { declared := type_from_syntax(checker, syntax, pkg, file) if types.is_constraint(declared) { return types.constraint_target(declared, actual, &checker.module.types) } return declared } can_specialize :: proc( checker: ^Checker, function: ast.Function, actual_args: []types.Type, comptime_values: []Comptime_Value = nil, ) -> bool { previous_comptime := checker.current_comptime_values checker.current_comptime_values = comptime_values defer checker.current_comptime_values = previous_comptime for param, index in function.params { if param.comptime_value { continue } actual := types.INVALID if index < len(actual_args) { actual = actual_args[index] } if !is_runtime_type(checker, specialized_param_type(checker, param.type, actual, function.pkg, function.file)) { return false } } return true } ensure_spec :: proc( checker: ^Checker, template: ast.Function_Id, actual_args: []types.Type, comptime_values: []Comptime_Value = nil, ) -> Spec_Id { if existing := find_spec(checker, template, actual_args, comptime_values); existing != INVALID_SPEC { return existing } function := checker.ast_module.functions[template] previous_comptime := checker.current_comptime_values checker.current_comptime_values = comptime_values defer checker.current_comptime_values = previous_comptime signature: [dynamic]types.Type signature.allocator = checker.allocator for param, index in function.params { if param.comptime_value { continue } actual := types.INVALID if index < len(actual_args) { actual = actual_args[index] } append(&signature, specialized_param_type(checker, param.type, actual, function.pkg, function.file)) } comptime_signature: [dynamic]Comptime_Value comptime_signature.allocator = checker.allocator append(&comptime_signature, ..comptime_values) result := function_channel_type(checker, function) if function.pkg == 0 && function.name == checker.main_symbol && function.result == types.INT && !types.is_valid(function.error) { result = types.I32 } index := spec_id(len(checker.specs)) append( &checker.specs, Spec{ template = template, args = signature[:], comptime_values = comptime_signature[:], result = result, hir_id = hir.INVALID_FUNCTION, }, ) return index } mark_spec_demanded :: proc(checker: ^Checker, id: Spec_Id, stack: ^[dynamic]Spec_Id) { if id == INVALID_SPEC || checker.specs[id].hir_id != hir.INVALID_FUNCTION { return } checker.specs[id].hir_id = hir.Function_Id(0) append(stack, id) } Infer_Frame :: struct { expr: ast.Expr_Id, expected: types.Type, stage: u8, left: types.Type, arg_index: int, arg_mode: Call_Argument_Mode, prefix: int, args: []types.Type, template: ast.Function_Id, } infer_nested_expr :: proc( checker: ^Checker, expr_id: ast.Expr_Id, locals: []Infer_Local, pkg: ast.Package_Id, file: ast.File_Id, demanded: ^[dynamic]Spec_Id, local_types: []types.Type = nil, expected := types.INVALID, ) -> types.Type { outer := checker.infer_stack checker.infer_stack = nil checker.infer_stack.allocator = checker.allocator result := infer_expr(checker, expr_id, locals, pkg, file, demanded, local_types, expected) delete(checker.infer_stack) checker.infer_stack = outer return result } infer_compound_expr :: proc( checker: ^Checker, expr: ast.Expr, locals: []Infer_Local, pkg: ast.Package_Id, file: ast.File_Id, demanded: ^[dynamic]Spec_Id, local_types: []types.Type = nil, expected := types.INVALID, ) -> types.Type { store := &checker.module.types #partial switch expr.kind { case .Comptime: return infer_comptime_expr_type(checker, expr, pkg, file, demanded) case .Bool: return types.BOOL case .Not: _ = infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types) return types.BOOL case .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or: _ = infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types) _ = infer_nested_expr(checker, expr.right, locals, pkg, file, demanded, local_types) return types.BOOL case .Range: left := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types) right := infer_nested_expr(checker, expr.right, locals, pkg, file, demanded, local_types) left_const := eval_constant(checker, expr.left) right_const := eval_constant(checker, expr.right) child := types.INVALID if left_const.kind == .Value && right_const.kind != .Value { child = right } else if right_const.kind == .Value && left_const.kind != .Value { child = left } else { child = types.widest(left, right) } if !types.is_concrete_integer(child) { return types.INVALID } return types.range(store, child) case .String: return string_literal_type(checker, expr.integer) case .Array: element := types.INVALID for arg in expr.args { actual := infer_nested_expr(checker, arg, locals, pkg, file, demanded, local_types) if !types.is_valid(element) { element = actual } else if !types.equal(element, actual) { element = types.widest(element, actual) } } if !types.is_valid(element) { element = types.I64 } return types.array(store, element, u64(len(expr.args)), false) case .None: return types.INVALID case .Undefined: return types.INVALID case .Enum_Literal: if expr.left != ast.INVALID_EXPR { _ = infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types) } return types.INVALID case .Cast: _ = infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types) return type_from_syntax(checker, expr.type, pkg, file) case .Address: child := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types) return types.pointer(store, child, false, false) case .Deref: value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types) return types.child_type(value, store) if types.is_pointer(value, store) else types.INVALID case .Index: value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types) _ = infer_nested_expr(checker, expr.right, locals, pkg, file, demanded, local_types) item, ok := types.container(value, store) return item.child if ok else types.INVALID case .Slice: value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types) item, ok := types.container(value, store) if !ok || (item.kind == .Pointer && expr.args[1] == ast.INVALID_EXPR) { return types.INVALID } for bound in expr.args { if bound != ast.INVALID_EXPR { _ = infer_nested_expr(checker, bound, locals, pkg, file, demanded, local_types) } } preserve := item.has_sentinel && expr.args[1] == ast.INVALID_EXPR return types.slice(store, item.child, item.mutable, preserve, item.sentinel) case .Field: if enum_type, enum_ok := enum_type_from_field_expr(checker, expr, pkg, file); enum_ok { _, member_ok := find_enum_member(checker, enum_type, expr.name) return enum_type if member_ok else types.INVALID } value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types) field_name := symbol_text(checker, expr.name) item, has_item := types.container(value, store) if has_item && (item.kind == .Array || item.kind == .Slice) { if field_name == "len" { return types.USIZE } if field_name == "ptr" && (item.kind == .Slice || types.is_pointer(value, store)) { return container_pointer_type(store, item) } } if types.is_pointer(value, store) { value = types.child_type(value, store) } _, field, ok := find_struct_field(checker, value, expr.name) return field.type if ok else types.INVALID case .Unwrap: value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types) return types.child_type(value, store) if types.is_optional(value, store) else types.INVALID case .Orelse: value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types) _ = infer_nested_expr(checker, expr.right, locals, pkg, file, demanded, local_types) return types.child_type(value, store) if types.is_optional(value, store) else types.INVALID case .Try: left_expected := expected if checker.ast_module.exprs[expr.left].kind == .Call else types.INVALID value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types, left_expected) return types.fallible_success(value, store) if types.kind(value, store) == .Fallible else types.INVALID case .Catch: left_expected := expected if checker.ast_module.exprs[expr.left].kind == .Call else types.INVALID value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types, left_expected) success := types.fallible_success(value, store) error_type := types.fallible_error(value, store) if expr.right != ast.INVALID_EXPR { fallback := infer_nested_expr(checker, expr.right, locals, pkg, file, demanded, local_types) if types.is_valid(success) && types.is_valid(fallback) && !types.equal(success, fallback) { return types.widest(success, fallback) } return success if types.is_valid(success) else fallback } block_locals: [dynamic]Infer_Local block_locals.allocator = checker.allocator defer delete(block_locals) append(&block_locals, ..locals) capture_start := len(block_locals) if symbol.is_valid(expr.name) && expr.name != checker.sink_symbol && types.is_valid(error_type) { append(&block_locals, Infer_Local{name=expr.name, type=error_type, declared=error_type, statement=ast.INVALID_STMT}) } infer_statements(checker, expr.body, &block_locals, local_types, pkg, file, demanded, &success, success) resize(&block_locals, capture_start) return success case .Struct_Literal: for keyed in expr.args { _ = infer_nested_expr(checker, checker.ast_module.exprs[keyed].left, locals, pkg, file, demanded, local_types) } if !symbol.is_valid(expr.name) { return types.INVALID } target_pkg, available := expr_package(checker, expr, pkg, file) value := types.find_named(store, u32(target_pkg), u32(expr.name), file=u32(expr_lookup_file(expr, file))) if available else types.INVALID return types.resolve_alias(value, store) case .Keyed: return infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types) case: return types.INVALID } } infer_expr :: proc( checker: ^Checker, expr_id: ast.Expr_Id, locals: []Infer_Local, pkg := ast.Package_Id(0), file := ast.File_Id(0), demanded: ^[dynamic]Spec_Id = nil, local_types: []types.Type = nil, expected := types.INVALID, ) -> types.Type { stack := checker.infer_stack clear_dynamic_array(&stack) defer { for frame in stack { delete(frame.args, checker.allocator) } clear_dynamic_array(&stack) checker.infer_stack = stack } append(&stack, Infer_Frame{expr=expr_id, expected=expected, template=ast.INVALID_FUNCTION}) last := types.INVALID for len(stack) > 0 { frame_index := len(stack)-1 frame := stack[frame_index] if frame.expr == ast.INVALID_EXPR || int(frame.expr) >= len(checker.ast_module.exprs) { last = types.INVALID _ = pop(&stack) continue } expr := checker.ast_module.exprs[frame.expr] if frame.stage == 0 { constant := eval_constant(checker, frame.expr) if constant.kind == .Overflow || constant.kind == .Div_By_Zero || (constant.kind == .Value && !fits_i64(constant.value)) { last = types.I64 _ = pop(&stack) continue } if constant.kind == .Value { last = types.smallest_signed_for_literal(i64(constant.value)) _ = pop(&stack) continue } switch expr.kind { case .Invalid: last = types.INVALID _ = pop(&stack) case .Type: last = types.INVALID _ = pop(&stack) case .Anonymous_Struct_Type: last = types.INVALID _ = pop(&stack) case .Integer: last = types.I64 if expr.integer <= 0x7fff_ffff_ffff_ffff { last = types.smallest_signed_for_literal(i64(expr.integer)) } _ = pop(&stack) case .Float: last = types.F64 _ = pop(&stack) case .String, .Array, .None, .Undefined, .Address, .Deref, .Index, .Slice, .Field, .Unwrap, .Orelse, .Try, .Catch, .Struct_Literal, .Keyed, .Enum_Literal, .Cast, .Comptime, .Bool, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range: last = infer_compound_expr(checker, expr, locals, pkg, file, demanded, local_types, frame.expected) _ = pop(&stack) case .Function_Literal: template := ast.Function_Id(u32(expr.integer)) pointer_type, _, ok := function_pointer_type_for_template(checker, template, demanded) last = pointer_type if ok else types.INVALID _ = pop(&stack) case .Name: last = types.INVALID if !symbol.is_valid(expr.qualifier) { last = find_infer_local(locals, expr.name) } else { base_type := find_infer_local(locals, expr.qualifier) item, has_item := types.container(base_type, &checker.module.types) field_name := symbol_text(checker, expr.name) if has_item && (item.kind == .Array || item.kind == .Slice) { if field_name == "len" { last = types.USIZE } else if field_name == "ptr" && (item.kind == .Slice || types.is_pointer(base_type, &checker.module.types)) { last = container_pointer_type(&checker.module.types, item) } } if types.is_pointer(base_type, &checker.module.types) { base_type = types.child_type(base_type, &checker.module.types) } if !types.is_valid(last) { _, field, ok := find_struct_field(checker, base_type, expr.name) if ok { last = field.type } } } if !types.is_valid(last) && symbol.is_valid(expr.qualifier) && find_import(checker, file, expr.qualifier) == ast.INVALID_IMPORT { if global := find_global(checker, expr.qualifier, pkg, file); global != ast.INVALID_GLOBAL { base_type := checker.global_types[global] item, has_item := types.container(base_type, &checker.module.types) field_name := symbol_text(checker, expr.name) if has_item && (item.kind == .Array || item.kind == .Slice) { if field_name == "len" { last = types.USIZE } else if field_name == "ptr" && (item.kind == .Slice || types.is_pointer(base_type, &checker.module.types)) { last = container_pointer_type(&checker.module.types, item) } } if types.is_pointer(base_type, &checker.module.types) { base_type = types.child_type(base_type, &checker.module.types) } if !types.is_valid(last) { _, field, ok := find_struct_field(checker, base_type, expr.name) if ok { last = field.type } } } } if !types.is_valid(last) { if !symbol.is_valid(expr.qualifier) { if value, ok := current_comptime_value(checker, expr.name); ok { if value.kind == .Integer { last = value.type } } } } if !types.is_valid(last) { if enum_type, enum_ok := enum_type_from_name_expr(checker, expr, pkg, file); enum_ok { _, member_ok := find_enum_member(checker, enum_type, expr.name) if member_ok { last = enum_type } } } if !types.is_valid(last) { target_pkg, available := expr_package(checker, expr, pkg, file) if available { global := find_global(checker, expr.name, target_pkg, expr_lookup_file(expr, file)) if global != ast.INVALID_GLOBAL { last = checker.global_types[global] } } } if !types.is_valid(last) { target_pkg, available := expr_package(checker, expr, pkg, file) if available { template := find_template(checker, expr.name, target_pkg, expr_lookup_file(expr, file)) if template != ast.INVALID_FUNCTION && len(checker.ast_module.functions[template].unsupported_reason) == 0 && checker.template_diagnostics[template] == source.INVALID_DIAGNOSTIC { pointer_type, _, ok := function_pointer_type_for_template(checker, template, demanded) if ok { last = pointer_type } } } } _ = pop(&stack) case .Negate: stack[frame_index].stage = 5 append(&stack, Infer_Frame{expr=expr.left, template=ast.INVALID_FUNCTION}) case .Add, .Sub, .Mul, .Div: stack[frame_index].stage = 1 append(&stack, Infer_Frame{expr=expr.left, template=ast.INVALID_FUNCTION}) case .Call: if expr.left != ast.INVALID_EXPR { callee_type := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types) _, function_item, function_type, ok := types.function_pointer(callee_type, &checker.module.types) if !ok { last = types.INVALID _ = pop(&stack) continue } stack[frame_index].left = function_type stack[frame_index].args = make([]types.Type, len(expr.args), checker.allocator) stack[frame_index].stage = 6 if len(expr.args) > 0 { append(&stack, Infer_Frame{expr=expr.args[0], template=ast.INVALID_FUNCTION}) } else if valid_callable_arity(function_item, 0) { last = function_item.child delete(stack[frame_index].args, checker.allocator) stack[frame_index].args = nil _ = pop(&stack) } continue } if builtin := type_builtin_call(checker, expr); builtin != .None { if builtin == .Size_Of || builtin == .Align_Of { last = types.USIZE } else if len(expr.args) == 1 { target, ok := resolve_type_argument(checker, expr.args[0], pkg, file) last = target if ok && types.is_concrete_integer(target) else types.INVALID } else { last = types.INVALID } _ = pop(&stack) continue } if is_ptr_cast_call(checker, expr) { if len(expr.args) != 2 { last = types.INVALID _ = pop(&stack) continue } child, child_ok := resolve_type_argument(checker, expr.args[0], pkg, file) operand := infer_nested_expr(checker, expr.args[1], locals, pkg, file, demanded, local_types) result := types.INVALID if child_ok && valid_ptr_cast_child(checker, child) { result, _ = types.replace_pointer_child(&checker.module.types, operand, child) } last = result _ = pop(&stack) continue } if callee_type, handled := infer_qualified_value_field_type(checker, expr, locals, pkg, file); handled { _, function_item, function_type, ok := types.function_pointer(callee_type, &checker.module.types) if !ok { last = types.INVALID _ = pop(&stack) continue } stack[frame_index].left = function_type stack[frame_index].args = make([]types.Type, len(expr.args), checker.allocator) stack[frame_index].stage = 6 if len(expr.args) > 0 { append(&stack, Infer_Frame{expr=expr.args[0], template=ast.INVALID_FUNCTION}) } else if valid_callable_arity(function_item, 0) { last = function_item.child delete(stack[frame_index].args, checker.allocator) stack[frame_index].args = nil _ = pop(&stack) } continue } target_pkg, available := expr_package(checker, expr, pkg, file) template := ast.INVALID_FUNCTION if available { template = find_template(checker, expr.name, target_pkg, expr_lookup_file(expr, file)) } if template == ast.INVALID_FUNCTION { callee_type := types.INVALID if !symbol.is_valid(expr.qualifier) { callee_type = find_infer_local(locals, expr.name) } if !types.is_valid(callee_type) && available { global := find_global(checker, expr.name, target_pkg, expr_lookup_file(expr, file)) if global != ast.INVALID_GLOBAL { callee_type = checker.global_types[global] } } _, function_item, function_type, ok := types.function_pointer(callee_type, &checker.module.types) if !ok { distinct_type := types.find_named(&checker.module.types, u32(target_pkg), u32(expr.name), file=u32(expr_lookup_file(expr, file))) distinct_item, distinct_ok := types.node(&checker.module.types, distinct_type) if available && distinct_ok && distinct_item.kind == .Distinct && len(expr.args) == 1 { stack[frame_index].left = distinct_type stack[frame_index].stage = 7 append(&stack, Infer_Frame{expr=expr.args[0], template=ast.INVALID_FUNCTION}) continue } last = types.INVALID _ = pop(&stack) continue } stack[frame_index].left = function_type stack[frame_index].args = make([]types.Type, len(expr.args), checker.allocator) stack[frame_index].stage = 6 if len(expr.args) > 0 { append(&stack, Infer_Frame{expr=expr.args[0], template=ast.INVALID_FUNCTION}) } else if valid_callable_arity(function_item, 0) { last = function_item.child delete(stack[frame_index].args, checker.allocator) stack[frame_index].args = nil _ = pop(&stack) } continue } if len(checker.ast_module.functions[template].unsupported_reason) > 0 { last = types.INVALID _ = pop(&stack) continue } if checker.template_diagnostics[template] != source.INVALID_DIAGNOSTIC { declared := function_channel_type(checker, checker.ast_module.functions[template]) last = declared if is_runtime_type(checker, declared) || types.is_void(declared) else types.INVALID _ = pop(&stack) continue } function := checker.ast_module.functions[template] mode, prefix := call_argument_mode(function, len(expr.args)) if mode == .Invalid { last = types.INVALID _ = pop(&stack) continue } stack[frame_index].template = template stack[frame_index].arg_mode = mode stack[frame_index].prefix = prefix stack[frame_index].args = make([]types.Type, len(function.params), checker.allocator) stack[frame_index].arg_index = next_runtime_call_arg(function, mode, prefix, 0, len(expr.args)) stack[frame_index].stage = 3 if stack[frame_index].arg_index < len(expr.args) { param_index := call_param_index(mode, prefix, stack[frame_index].arg_index) arg_expected := call_arg_expected(checker, function, param_index) if !is_runtime_type(checker, arg_expected) { arg_expected = types.INVALID } append(&stack, Infer_Frame{expr=expr.args[stack[frame_index].arg_index], expected=arg_expected, template=ast.INVALID_FUNCTION}) } } continue } if frame.stage == 5 { if !types.is_signed(last, checker.target) && !types.is_float(last, checker.target) { last = types.INVALID } _ = pop(&stack) continue } if frame.stage == 1 { stack[frame_index].left = last stack[frame_index].stage = 2 append(&stack, Infer_Frame{expr=expr.right, template=ast.INVALID_FUNCTION}) continue } if frame.stage == 2 { right := last if expr.kind == .Add && types.is_many_pointer(frame.left, &checker.module.types) && types.is_concrete_integer(right) { last = frame.left } else if is_numeric_constant_expr(checker, expr.right) && is_numeric_demand(frame.left, checker.target) && expr_accepts_numeric_demand(checker, expr.right, frame.left, locals, pkg, file) { last = frame.left } else if is_numeric_constant_expr(checker, expr.left) && is_numeric_demand(right, checker.target) && expr_accepts_numeric_demand(checker, expr.left, right, locals, pkg, file) { last = right } else if is_numeric_demand(frame.left, checker.target) && !numeric_operand_is_open(checker, expr.left, locals, pkg, file) && expr_accepts_numeric_demand(checker, expr.right, frame.left, locals, pkg, file) { // Propagate only from an authoritative (fixed-type) left operand. A left // operand that is still a provisional open constant carries only its // smallest-signed default, which must not poison the sibling's family; // two provisional operands are resolved together by the backward demand // from the declaration/use. _ = record_demand(checker, expr.right, frame.left, locals, local_types, pkg, file) last = frame.left } else if is_numeric_demand(right, checker.target) && !numeric_operand_is_open(checker, expr.right, locals, pkg, file) && expr_accepts_numeric_demand(checker, expr.left, right, locals, pkg, file) { _ = record_demand(checker, expr.left, right, locals, local_types, pkg, file) last = right } else { last = types.widest(frame.left, right) } _ = pop(&stack) continue } if frame.stage == 3 { if frame.arg_index < len(expr.args) { param_index := call_param_index(frame.arg_mode, frame.prefix, frame.arg_index) if param_index < len(stack[frame_index].args) { stack[frame_index].args[param_index] = last } next := next_runtime_call_arg( checker.ast_module.functions[frame.template], frame.arg_mode, frame.prefix, frame.arg_index+1, len(expr.args), ) stack[frame_index].arg_index = next if next < len(expr.args) { next_param := call_param_index(frame.arg_mode, frame.prefix, next) arg_expected := call_arg_expected(checker, checker.ast_module.functions[frame.template], next_param) if !is_runtime_type(checker, arg_expected) { arg_expected = types.INVALID } append(&stack, Infer_Frame{expr=expr.args[next], expected=arg_expected, template=ast.INVALID_FUNCTION}) continue } } function := checker.ast_module.functions[frame.template] // Deferred defaulting leaves an undemanded open constant typeless; give such an // argument its default so the call can still monomorphize (the default feeds only // the spec arg vector, not a demand). for source_index in 0..= len(function.params) || function.params[param_index].comptime_value { continue } demand := call_arg_expected(checker, function, param_index) record_demand(checker, expr.args[source_index], demand, locals, local_types, pkg, file) } checker.current_comptime_values = previous_comptime spec := INVALID_SPEC if demanded == nil { spec = ensure_spec(checker, frame.template, stack[frame_index].args, comptime_values) } else { spec = find_spec(checker, frame.template, stack[frame_index].args, comptime_values) mark_spec_demanded(checker, spec, demanded) } if spec != INVALID_SPEC { last = checker.specs[spec].result } else { previous_comptime := checker.current_comptime_values checker.current_comptime_values = comptime_values declared := function_channel_type(checker, function) checker.current_comptime_values = previous_comptime last = declared if is_runtime_type(checker, declared) || types.is_void(declared) else types.INVALID } } else { declared := function_channel_type(checker, function) if function.pkg == 0 && function.name == checker.main_symbol && function.result == types.INT && !types.is_valid(function.error) { last = types.I32 } else { last = declared if is_runtime_type(checker, declared) || types.is_void(declared) else types.INVALID } } delete(stack[frame_index].args, checker.allocator) stack[frame_index].args = nil _ = pop(&stack) } if frame.stage == 6 { if frame.arg_index < len(expr.args) { stack[frame_index].args[frame.arg_index] = last stack[frame_index].arg_index += 1 if frame.arg_index+1 < len(expr.args) { append(&stack, Infer_Frame{expr=expr.args[frame.arg_index+1], template=ast.INVALID_FUNCTION}) continue } } function_item, ok := types.node(&checker.module.types, frame.left) if ok && function_item.kind == .Function && valid_callable_arity(function_item, len(expr.args)) { last = function_item.child } else { last = types.INVALID } delete(stack[frame_index].args, checker.allocator) stack[frame_index].args = nil _ = pop(&stack) } if frame.stage == 7 { last = frame.left _ = pop(&stack) } } return last } flatten_conditional_unwrap_operands :: proc( module: ^ast.Module, expr_id: ast.Expr_Id, operands: ^[dynamic]ast.Expr_Id, ) { if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(module.exprs) { append(operands, expr_id) return } expr := module.exprs[expr_id] if expr.kind == .And { flatten_conditional_unwrap_operands(module, expr.left, operands) flatten_conditional_unwrap_operands(module, expr.right, operands) return } append(operands, expr_id) } record_infer_local_type :: proc(local: Infer_Local, local_types: []types.Type) { if local.statement != ast.INVALID_STMT && int(local.statement) < len(local_types) { local_types[local.statement] = local.type } } merge_infer_local_type :: proc( checker: ^Checker, local: ^Infer_Local, inferred: types.Type, local_types: []types.Type, ) -> bool { if !is_runtime_type(checker, inferred) { return false } if types.is_constraint(local.declared) { if !types.constraint_accepts(local.declared, inferred, &checker.module.types) { return false } if !is_runtime_type(checker, local.type) { local.type = inferred record_infer_local_type(local^, local_types) return true } if types.equal(local.type, inferred) { return false } merged := types.widest(local.type, inferred) if types.constraint_accepts(local.declared, merged, &checker.module.types) { local.type = merged record_infer_local_type(local^, local_types) return true } return false } if is_runtime_type(checker, local.declared) { local.type = local.declared record_infer_local_type(local^, local_types) return false } if !is_runtime_type(checker, local.type) { local.type = inferred record_infer_local_type(local^, local_types) return true } if types.equal(local.type, inferred) { return false } merged := types.widest(local.type, inferred) if types.is_concrete_scalar(merged) { local.type = merged record_infer_local_type(local^, local_types) return true } return false } infer_statements :: proc( checker: ^Checker, statements: []ast.Stmt_Id, locals: ^[dynamic]Infer_Local, local_types: []types.Type, pkg: ast.Package_Id, file: ast.File_Id, demanded: ^[dynamic]Spec_Id, result: ^types.Type, result_hint := types.INVALID, ) { scope_start := len(locals^) for statement_id in statements { statement := checker.ast_module.statements[statement_id] #partial switch statement.kind { case .Declaration: if statement.expr == ast.INVALID_EXPR { // Value block (`x :: { ... yield v }` / `x T = { ... }`): register the // binding (its declared type when annotated, else left open) and walk // the block body. The build pass resolves the yielded value's type // independently — value blocks don't join the demand fixpoint. declared_block := type_from_syntax(checker, statement.type, pkg, file) block_type := declared_block if is_runtime_type(checker, declared_block) else types.INVALID local := Infer_Local{ name=statement.name, type=block_type, declared=declared_block, statement=statement_id, mutable=!statement.immutable, } append(locals, local) record_infer_local_type(local, local_types) infer_statements(checker, statement.body, locals, local_types, pkg, file, demanded, result, result_hint) continue } declared_local := resolve_inferred_array(checker, type_from_syntax(checker, statement.type, pkg, file), statement.expr) value_type := types.INVALID if !is_undefined_expr(checker, statement.expr) { expected := declared_local if is_runtime_type(checker, declared_local) else types.INVALID value_type = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types, expected) declared_local = resolve_inferred_array_from_type(checker, declared_local, value_type) } if is_runtime_type(checker, declared_local) && !has_inferred_array_count(checker, declared_local) { value_type = declared_local } else if types.is_constraint(declared_local) { // Seed the binding in-family (INVALID on mismatch, which // build_block reports). FLOAT defaults integers to f64. value_type = types.constraint_target(declared_local, value_type, &checker.module.types) } open := false open_float := false const_val := i128(0) if !is_runtime_type(checker, declared_local) && !is_undefined_expr(checker, statement.expr) { constant := eval_integer_constant_in_context(checker, statement.expr, pkg, file) if constant.kind == .Value && fits_i64(constant.value) { open = true const_val = constant.value } else if is_float_constant_expr(checker, statement.expr) { open_float = true } } local := Infer_Local{ name=statement.name, type=value_type, declared=declared_local, statement=statement_id, mutable=!statement.immutable, open_const=open, open_float=open_float, const_value=const_val, } append(locals, local) record_infer_local_type(local, local_types) // A typed/constraint declaration initialized by a bare name pushes its resolved // type backward onto that name (mirrors the `Y int :: X; Z i32 :: Y` global chain). // Skip this for a constraint/untyped declaration whose initializer is arithmetic: // value_type is then only a provisional default (e.g. `x int = a - b` resolving to // i16 before the operands' real uses are seen) and would poison the open-constant // operands' family. The operands resolve from their own authoritative uses, and the // local adopts their resolved type forward. Concrete-declared arithmetic (e.g. // `b u16 :: a + 2`) still pushes, since value_type is the concrete declared type. init_is_arith := is_arith_kind(checker.ast_module.exprs[statement.expr].kind) if is_runtime_type(checker, declared_local) || !init_is_arith { record_demand(checker, statement.expr, value_type, locals^[:], local_types, pkg, file) } case .Assignment: if statement.expr == ast.INVALID_EXPR { // Value block assigned to a target: walk the block body; the build // pass handles the target coercion. infer_statements(checker, statement.body, locals, local_types, pkg, file, demanded, result, result_hint) continue } expected_assignment := types.INVALID if statement.target != ast.INVALID_EXPR { expected_assignment = infer_expr(checker, statement.target, locals^[:], pkg, file, demanded, local_types) } else if statement.name != checker.sink_symbol { if local_index, ok := find_infer_local_index(locals^[:], statement.name); ok { expected_assignment = locals^[local_index].type } else if global := find_global(checker, statement.name, pkg, file); global != ast.INVALID_GLOBAL { expected_assignment = checker.global_types[global] } } value_type := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types, expected_assignment) // Only push the target's type back onto a bare-name RHS (e.g. `x += speed`): // pushing through an arithmetic RHS would feed the target's (often provisional) // type onto open-constant operands and poison their family. Operands of an // arithmetic RHS resolve from their own authoritative uses. rhs_is_arith := is_arith_kind(checker.ast_module.exprs[statement.expr].kind) if statement.target != ast.INVALID_EXPR { target_type := expected_assignment target_expr := checker.ast_module.exprs[statement.target] if target_expr.kind == .Name && !symbol.is_valid(target_expr.qualifier) { if local_index, ok := find_infer_local_index(locals^[:], target_expr.name); ok && locals^[local_index].mutable { _ = merge_infer_local_type(checker, &locals^[local_index], value_type, local_types) } } if !rhs_is_arith { _ = record_demand(checker, statement.expr, target_type, locals^[:], local_types, pkg, file) } } else if statement.name != checker.sink_symbol { if local_index, ok := find_infer_local_index(locals^[:], statement.name); ok && locals^[local_index].mutable { _ = merge_infer_local_type(checker, &locals^[local_index], value_type, local_types) if !rhs_is_arith { _ = record_demand(checker, statement.expr, locals^[local_index].type, locals^[:], local_types, pkg, file) } } else if global := find_global(checker, statement.name, pkg, file); global != ast.INVALID_GLOBAL { _ = merge_global_demand(checker, global, value_type) if !rhs_is_arith { _ = record_demand(checker, statement.expr, checker.global_types[global], locals^[:], local_types, pkg, file) } } } case .Expression: _ = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types) case .Yield: if statement.expr != ast.INVALID_EXPR { _ = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types) } case .Return: if statement.expr != ast.INVALID_EXPR { returned := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types, result_hint) if is_runtime_type(checker, result_hint) { _ = record_demand_shallow(checker, statement.expr, result_hint, locals^[:], local_types, pkg, file) expr := checker.ast_module.exprs[statement.expr] if expr.kind == .Name && !symbol.is_valid(expr.qualifier) { if local_index, ok := find_infer_local_index(locals^[:], expr.name); ok { // Open constants can adopt the result type cross-family; other // locals widen within family as before. if !merge_local_demand(checker, &locals^[local_index], result_hint, local_types) { _ = merge_infer_local_type(checker, &locals^[local_index], result_hint, local_types) } returned = result_hint } else { // `return G` for a global const: demand the result type onto it. record_demand_shallow(checker, statement.expr, result_hint, locals^[:], local_types, pkg, file) } } } if !types.is_valid(result^) { result^ = returned } else if !types.equal(result^, returned) { result^ = types.widest(result^, returned) } } case .If: if len(statement.captures) > 0 { operands: [dynamic]ast.Expr_Id operands.allocator = checker.allocator flatten_conditional_unwrap_operands(checker.ast_module, statement.expr, &operands) operand_types := make([]types.Type, len(operands), checker.allocator) for operand, index in operands { operand_types[index] = infer_expr(checker, operand, locals^[:], pkg, file, demanded, local_types) } capture_start := len(locals^) for capture, index in statement.captures { if capture == checker.sink_symbol { continue } capture_type := types.INVALID if index < len(operand_types) && types.is_optional(operand_types[index], &checker.module.types) { capture_type = types.child_type(operand_types[index], &checker.module.types) } append(locals, Infer_Local{name=capture, type=capture_type, declared=capture_type, statement=ast.INVALID_STMT}) } if statement.guard != ast.INVALID_EXPR { _ = infer_expr(checker, statement.guard, locals^[:], pkg, file, demanded, local_types) } infer_statements(checker, statement.body, locals, local_types, pkg, file, demanded, result, result_hint) resize(locals, capture_start) infer_statements(checker, statement.else_body, locals, local_types, pkg, file, demanded, result, result_hint) delete(operand_types, checker.allocator) delete(operands) } else { _ = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types) infer_statements(checker, statement.body, locals, local_types, pkg, file, demanded, result, result_hint) infer_statements(checker, statement.else_body, locals, local_types, pkg, file, demanded, result, result_hint) } case .While: _ = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types) infer_statements(checker, statement.body, locals, local_types, pkg, file, demanded, result, result_hint) if statement.update != ast.INVALID_STMT { update := [1]ast.Stmt_Id{statement.update} infer_statements(checker, update[:], locals, local_types, pkg, file, demanded, result, result_hint) } case .For: iterable_type := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types) capture_start := len(locals^) capture_type := types.INVALID if types.is_range(iterable_type, &checker.module.types) { capture_type = types.child_type(iterable_type, &checker.module.types) } else { item, ok := sequence_item(iterable_type, &checker.module.types) if ok { capture_type = item.child if statement.pointer_capture { capture_type = types.pointer(&checker.module.types, item.child, item.mutable, false) } } } if symbol.is_valid(statement.name) { append(locals, Infer_Local{name=statement.name, type=capture_type, declared=capture_type, statement=ast.INVALID_STMT}) } if symbol.is_valid(statement.index_name) { append(locals, Infer_Local{name=statement.index_name, type=types.USIZE, declared=types.USIZE, statement=ast.INVALID_STMT}) } infer_statements(checker, statement.body, locals, local_types, pkg, file, demanded, result, result_hint) resize(locals, capture_start) case .Block: infer_statements(checker, statement.body, locals, local_types, pkg, file, demanded, result, result_hint) case .Defer: deferred := [1]ast.Stmt_Id{statement.update} infer_statements(checker, deferred[:], locals, local_types, pkg, file, demanded, result, result_hint) case .Match: // The build pass desugars `match` to an if/else chain, but inference runs first // and must still visit the subject and arm bodies so calls there get specialized // (e.g. `match get()`). Mirror the `.For`/unwrap-`.If` capture handling. subject_type := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types) is_tagged := types.is_tagged_union(subject_type, &checker.module.types) for arm_id in statement.body { arm := checker.ast_module.statements[arm_id] if arm.kind != .Match_Arm { continue } for pattern in arm.patterns { _ = infer_expr(checker, pattern, locals^[:], pkg, file, demanded, local_types) } capture_start := len(locals^) if len(arm.captures) > 0 && is_tagged && len(arm.patterns) > 0 { capture := arm.captures[0] if capture != checker.sink_symbol { capture_type := types.INVALID pattern := checker.ast_module.exprs[arm.patterns[0]] if pattern.kind == .Enum_Literal { if _, field, ok := find_struct_field(checker, subject_type, pattern.name); ok { capture_type = field.type if arm.pointer_capture { // Mutability is best-effort here; the build pass finalizes // the exact pointer type and coerces the captured value. capture_type = types.pointer(&checker.module.types, field.type, true, false) } } } append(locals, Infer_Local{name=capture, type=capture_type, declared=capture_type, statement=ast.INVALID_STMT}) } } infer_statements(checker, arm.body, locals, local_types, pkg, file, demanded, result, result_hint) resize(locals, capture_start) } } } resize(locals, scope_start) } infer_spec_locals_and_result :: proc( checker: ^Checker, id: Spec_Id, demanded: ^[dynamic]Spec_Id = nil, ) -> ([]types.Type, types.Type) { spec := checker.specs[id] function := checker.ast_module.functions[spec.template] previous_comptime := checker.current_comptime_values checker.current_comptime_values = spec.comptime_values defer checker.current_comptime_values = previous_comptime declared := type_from_syntax(checker, function.result, function.pkg, function.file) if function.pkg == 0 && function.name == checker.main_symbol && function.result == types.INT { declared = types.I32 } result_hint := declared if is_runtime_type(checker, declared) else types.INVALID locals: [dynamic]Infer_Local locals.allocator = checker.allocator defer delete(locals) local_types := make([]types.Type, len(checker.ast_module.statements), checker.allocator) runtime_index := 0 for param, index in function.params { if param.comptime_value { continue } param_type := types.INVALID if runtime_index < len(spec.args) { param_type = spec.args[runtime_index] } append(&locals, Infer_Local{name=param.name, type=param_type, declared=param_type, statement=ast.INVALID_STMT}) runtime_index += 1 } result := types.INVALID infer_statements(checker, function.body, &locals, local_types, function.pkg, function.file, demanded, &result, result_hint) if types.is_constraint(declared) { // Narrow the inferred result to the constraint's family; an out-of-family // result (e.g. returning a non-integer from an `int` function) yields // INVALID and is rejected downstream. return local_types, types.constraint_target(declared, result, &checker.module.types) } return local_types, declared } infer_spec_result :: proc(checker: ^Checker, id: Spec_Id, demanded: ^[dynamic]Spec_Id = nil) -> types.Type { local_types, result := infer_spec_locals_and_result(checker, id, demanded) delete(local_types, checker.allocator) return result } merge_inferred_type :: proc(store: ^types.Store, current: ^types.Type, inferred: types.Type) -> bool { if !types.is_runtime_value(inferred, store) { return false } if !types.is_runtime_value(current^, store) { current^ = inferred return true } if types.equal(current^, inferred) { return false } merged := types.widest(current^, inferred) if types.is_concrete_scalar(merged) && !types.equal(current^, merged) { current^ = merged return true } return false } open_integer_accepts_demand :: proc(checker: ^Checker, value: i128, demand: types.Type) -> bool { if types.is_concrete_integer(demand) { return fits_integer_type(value, demand, checker.target) } return types.is_float(demand, checker.target) } open_float_accepts_demand :: proc(checker: ^Checker, demand: types.Type) -> bool { return types.is_float(demand, checker.target) } // merge_open_const_demand records a concrete numeric demand onto an open numeric // global's slot. Integer constants may adopt integer or float demands; float // constants may adopt float demands. Later demands only widen within the chosen family. merge_open_const_demand :: proc( checker: ^Checker, slot: ^types.Type, demand: types.Type, int_open: bool, float_open: bool, value: i128, ) -> bool { if !(int_open && open_integer_accepts_demand(checker, value, demand) || float_open && open_float_accepts_demand(checker, demand)) { return false } if !is_runtime_type(checker, slot^) { slot^ = demand return true } if types.equal(slot^, demand) { return false } merged := types.widest(slot^, demand) if types.is_concrete_scalar(merged) && !types.equal(slot^, merged) { slot^ = merged return true } return false } // merge_global_demand routes a concrete demand onto a global's slot: open constants // adopt any fitting family, other referents widen within family. Sets a dirty flag so // a demand pushed from a function body re-triggers the inference fixpoint. merge_global_demand :: proc(checker: ^Checker, global: ast.Global_Id, demand: types.Type) -> bool { index := int(global) if index < 0 || index >= len(checker.global_demands) { return false } ast_global := checker.ast_module.globals[global] declared := type_from_syntax(checker, ast_global.type, ast_global.pkg, ast_global.file) if types.is_constraint(declared) && !types.constraint_accepts(declared, demand, &checker.module.types) { return false } changed: bool if checker.global_open_const[index] || checker.global_open_float[index] { changed = merge_open_const_demand( checker, &checker.global_demands[index], demand, checker.global_open_const[index], checker.global_open_float[index], checker.global_const_value[index], ) } else { changed = merge_inferred_type(&checker.module.types, &checker.global_demands[index], demand) } checker.global_demands_dirty = checker.global_demands_dirty || changed return changed } // merge_local_demand records a concrete numeric demand onto an open-constant local. // The first demand replaces the literal's default type; later demands may only widen // within the chosen family. merge_local_demand :: proc(checker: ^Checker, local: ^Infer_Local, demand: types.Type, local_types: []types.Type) -> bool { if !(local.open_const && open_integer_accepts_demand(checker, local.const_value, demand) || local.open_float && open_float_accepts_demand(checker, demand)) { return false } if types.is_constraint(local.declared) && !types.constraint_accepts(local.declared, demand, &checker.module.types) { return false } if !local.demanded { local.type = demand local.demanded = true record_infer_local_type(local^, local_types) return true } if types.equal(local.type, demand) { return false } merged := types.widest(local.type, demand) if types.is_concrete_scalar(merged) && !types.equal(local.type, merged) { local.type = merged record_infer_local_type(local^, local_types) return true } return false } is_arith_kind :: proc(k: ast.Expr_Kind) -> bool { return k == .Add || k == .Sub || k == .Mul || k == .Div || k == .Negate } // open_const_default_type returns the fallback type a bare open-constant reference // (local or global) would take if no use ever demands it: the smallest signed type // that holds an integer constant, or f64 for a float constant. Used to give a call's // argument a concrete type for monomorphization when deferred defaulting has left the // open constant typeless (its param is a constraint, so the call records no demand on // it). The fallback feeds only the specialization's arg vector, never a demand, so it // cannot leak back onto the constant or a sibling. open_const_default_type :: proc( checker: ^Checker, expr_id: ast.Expr_Id, locals: []Infer_Local, pkg: ast.Package_Id, file: ast.File_Id, ) -> types.Type { if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) { return types.INVALID } expr := checker.ast_module.exprs[expr_id] if expr.kind != .Name { return types.INVALID } if !symbol.is_valid(expr.qualifier) { if index, ok := find_infer_local_index(locals, expr.name); ok { local := locals[index] if local.open_const { return types.smallest_signed_for_literal(i64(local.const_value)) } if local.open_float { return types.F64 } return types.INVALID } } target_pkg, available := expr_package(checker, expr, pkg, file) if !available { return types.INVALID } global := find_global(checker, expr.name, target_pkg, expr_lookup_file(expr, file)) index := int(global) if global == ast.INVALID_GLOBAL || index < 0 || index >= len(checker.global_open_const) { return types.INVALID } if checker.global_open_const[index] { return types.smallest_signed_for_literal(i64(checker.global_const_value[index])) } if checker.global_open_float[index] { return types.F64 } return types.INVALID } // numeric_operand_is_open reports whether an arithmetic operand still carries a // provisional type (an open constant at its smallest-signed default, or a bare // numeric literal) rather than an authoritative one. A provisional operand must not // propagate its type onto a sibling open constant: doing so locks the sibling into a // default family and blocks the real backward demand from the declaration/use. Two // provisional operands are instead resolved together by that backward demand. numeric_operand_is_open :: proc( checker: ^Checker, expr_id: ast.Expr_Id, locals: []Infer_Local, pkg: ast.Package_Id, file: ast.File_Id, ) -> bool { if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) { return false } expr := checker.ast_module.exprs[expr_id] #partial switch expr.kind { case .Name: // A bare open-constant name is open iff it has a deferred default to assign. return is_runtime_type(checker, open_const_default_type(checker, expr_id, locals, pkg, file)) case .Negate: return numeric_operand_is_open(checker, expr.left, locals, pkg, file) case .Add, .Sub, .Mul, .Div: return numeric_operand_is_open(checker, expr.left, locals, pkg, file) || numeric_operand_is_open(checker, expr.right, locals, pkg, file) } // A bare integer/float literal adapts freely, so it too is provisional. return is_numeric_constant_expr(checker, expr_id) } expr_accepts_numeric_demand :: proc( checker: ^Checker, expr_id: ast.Expr_Id, demand: types.Type, locals: []Infer_Local, pkg: ast.Package_Id, file: ast.File_Id, ) -> bool { if !is_numeric_demand(demand, checker.target) || expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) { return false } if constant := eval_integer_constant_in_context(checker, expr_id, pkg, file); constant.kind == .Value { return open_integer_accepts_demand(checker, constant.value, demand) } if is_float_constant_expr(checker, expr_id) { return open_float_accepts_demand(checker, demand) } expr := checker.ast_module.exprs[expr_id] #partial switch expr.kind { case .Name: if !symbol.is_valid(expr.qualifier) { if index, ok := find_infer_local_index(locals, expr.name); ok { local := locals[index] return local.open_const && open_integer_accepts_demand(checker, local.const_value, demand) || local.open_float && open_float_accepts_demand(checker, demand) } } target_pkg, available := expr_package(checker, expr, pkg, file) if !available { return false } global := find_global(checker, expr.name, target_pkg, expr_lookup_file(expr, file)) index := int(global) if global == ast.INVALID_GLOBAL || index < 0 || index >= len(checker.global_open_const) { return false } return checker.global_open_const[index] && open_integer_accepts_demand(checker, checker.global_const_value[index], demand) || checker.global_open_float[index] && open_float_accepts_demand(checker, demand) case .Negate: if !types.is_signed(demand, checker.target) && !types.is_float(demand, checker.target) { return false } return expr_accepts_numeric_demand(checker, expr.left, demand, locals, pkg, file) case .Add, .Sub, .Mul, .Div: return expr_accepts_numeric_demand(checker, expr.left, demand, locals, pkg, file) && expr_accepts_numeric_demand(checker, expr.right, demand, locals, pkg, file) } return false } DEMAND_RECURSION_LIMIT :: 4096 record_demand_too_deep :: proc(checker: ^Checker, expr_id: ast.Expr_Id) -> bool { stack: [dynamic]ast.Expr_Id stack.allocator = checker.allocator defer delete(stack) append(&stack, expr_id) seen := 0 for len(stack) > 0 { current := pop(&stack) if current == ast.INVALID_EXPR || int(current) >= len(checker.ast_module.exprs) { continue } seen += 1 if seen > DEMAND_RECURSION_LIMIT { return true } expr := checker.ast_module.exprs[current] #partial switch expr.kind { case .Negate: append(&stack, expr.left) case .Add, .Sub, .Mul, .Div: append(&stack, expr.left, expr.right) } } return false } record_demand_shallow :: proc( checker: ^Checker, expr_id: ast.Expr_Id, demand: types.Type, locals: []Infer_Local, local_types: []types.Type, pkg: ast.Package_Id, file: ast.File_Id, ) -> bool { if record_demand_too_deep(checker, expr_id) { return false } return record_demand(checker, expr_id, demand, locals, local_types, pkg, file) } // record_demand pushes a concrete type demand onto open numeric slots reachable // through bare names and numeric arithmetic. Calls remain a boundary (milestone 14.5). record_demand :: proc( checker: ^Checker, expr_id: ast.Expr_Id, demand: types.Type, locals: []Infer_Local, local_types: []types.Type, pkg: ast.Package_Id, file: ast.File_Id, ) -> bool { if !is_runtime_type(checker, demand) || expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) { return false } expr := checker.ast_module.exprs[expr_id] #partial switch expr.kind { case .Name: if !symbol.is_valid(expr.qualifier) { if index, ok := find_infer_local_index(locals, expr.name); ok { return merge_local_demand(checker, &locals[index], demand, local_types) } } target_pkg, available := expr_package(checker, expr, pkg, file) if !available { return false } global := find_global(checker, expr.name, target_pkg, expr_lookup_file(expr, file)) if global != ast.INVALID_GLOBAL { return merge_global_demand(checker, global, demand) } case .Negate: if types.is_signed(demand, checker.target) || types.is_float(demand, checker.target) { return record_demand(checker, expr.left, demand, locals, local_types, pkg, file) } case .Add, .Sub, .Mul, .Div: if is_numeric_demand(demand, checker.target) { left := record_demand(checker, expr.left, demand, locals, local_types, pkg, file) right := record_demand(checker, expr.right, demand, locals, local_types, pkg, file) return left || right } } return false } infer_all :: proc(checker: ^Checker) { // An "open constant" global has no concrete declared type and a compile-time // numeric initializer. Its slot can adopt a backward demand from any reachable use. // Demands accumulate in global_demands so the default never blocks a later // cross-family demand (e.g. integer literal -> unsigned or float). for global, index in checker.ast_module.globals { declared := resolve_inferred_array( checker, type_from_syntax(checker, global.type, global.pkg, global.file), global.expr, ) if is_runtime_type(checker, declared) && !has_inferred_array_count(checker, declared) { checker.global_types[index] = declared continue } if global.external { continue } if global.expr != ast.INVALID_EXPR && int(global.expr) < len(checker.ast_module.exprs) { expr := checker.ast_module.exprs[global.expr] if builtin := type_builtin_call(checker, expr); builtin != .None { if builtin == .Size_Of || builtin == .Align_Of { checker.global_types[index] = types.USIZE } else if len(expr.args) == 1 { target, ok := resolve_type_argument(checker, expr.args[0], global.pkg, global.file) if ok && types.is_concrete_integer(target) { checker.global_types[index] = target } } continue } } constant := eval_integer_constant_in_context(checker, global.expr, global.pkg, global.file) if constant.kind == .Value && fits_i64(constant.value) { checker.global_open_const[index] = true checker.global_const_value[index] = constant.value } else if is_float_constant_expr(checker, global.expr) { checker.global_open_float[index] = true } } main_template := find_template(checker, checker.main_symbol, 0) if main_template != ast.INVALID_FUNCTION { ensure_spec(checker, main_template, nil) } defaults_applied := false for { changed := false checker.global_demands_dirty = false spec_count := len(checker.specs) // Backward demands: a global pushes its own (declared or already-resolved) type // onto open numeric slots reachable through names and numeric arithmetic. for global, index in checker.ast_module.globals { if global.external { continue } demand := checker.global_types[index] if !is_runtime_type(checker, demand) { continue } _ = record_demand(checker, global.expr, demand, nil, nil, global.pkg, global.file) } // Forward / resolution. infer_expr runs for every non-external global (even // concrete-typed ones) for its side effect of specializing called functions and // recording demands from call arguments in their initializers. for global, index in checker.ast_module.globals { if global.external { continue } declared := resolve_inferred_array( checker, type_from_syntax(checker, global.type, global.pkg, global.file), global.expr, ) expected := declared if is_runtime_type(checker, declared) else types.INVALID inferred := infer_expr(checker, global.expr, nil, global.pkg, global.file, expected=expected) if resolved := resolve_inferred_array_from_type(checker, declared, inferred); resolved != declared { if !types.equal(checker.global_types[index], resolved) { checker.global_types[index] = resolved changed = true } continue } if is_runtime_type(checker, declared) && !has_inferred_array_count(checker, declared) { continue } if types.is_constraint(declared) { resolved := types.INVALID if is_runtime_type(checker, checker.global_demands[index]) && types.constraint_accepts(declared, checker.global_demands[index], &checker.module.types) { resolved = checker.global_demands[index] } else { resolved = types.constraint_target(declared, inferred, &checker.module.types) } if is_runtime_type(checker, resolved) && !types.equal(checker.global_types[index], resolved) { checker.global_types[index] = resolved changed = true } continue } if is_runtime_type(checker, checker.global_demands[index]) { // A backward demand is authoritative; assign directly (it may cross the // signed/unsigned family that widening would reject). if !types.equal(checker.global_types[index], checker.global_demands[index]) { checker.global_types[index] = checker.global_demands[index] changed = true } } else if checker.global_open_const[index] || checker.global_open_float[index] { // Defer defaulting: an undemanded open constant stays typeless during the // fixpoint so its provisional smallest-signed default never leaks as a // demand and poisons a sibling open constant used in the same arithmetic // (e.g. the lagging type in `x += speed`). The pass after this loop assigns // the default once the fixpoint settles and no further demand can arrive. } else { changed = merge_inferred_type(&checker.module.types, &checker.global_types[index], inferred) || changed } } for index := 0; index < len(checker.specs); index += 1 { id := spec_id(index) inferred := infer_spec_result(checker, id) changed = merge_inferred_type(&checker.module.types, &checker.specs[id].result, inferred) || changed } if len(checker.specs) != spec_count { changed = true } // A demand pushed onto a global from inside a function body (via the spec loop) // is picked up by the next pass's resolution, so keep iterating for it. if checker.global_demands_dirty { changed = true } if !changed { if !defaults_applied { defaults_applied = true defaulted := false // No authoritative demand can still arrive. Assign final defaults, then // continue the same fixpoint so dependent globals/specs observe them. for global, index in checker.ast_module.globals { if global.external || is_runtime_type(checker, checker.global_types[index]) { continue } if checker.global_open_const[index] { checker.global_types[index] = types.smallest_signed_for_literal(i64(checker.global_const_value[index])) defaulted = true } else if checker.global_open_float[index] { checker.global_types[index] = types.F64 defaulted = true } } if defaulted { continue } } break } } } prune_specs :: proc(checker: ^Checker) { stack: [dynamic]Spec_Id stack.allocator = checker.allocator defer delete(stack) main_template := find_template(checker, checker.main_symbol, 0) if main_template != ast.INVALID_FUNCTION { mark_spec_demanded(checker, find_spec(checker, main_template, nil), &stack) } for global in checker.ast_module.globals { if global.external { continue } _ = infer_expr(checker, global.expr, nil, global.pkg, global.file, &stack) } for len(stack) > 0 { id := pop(&stack) _ = infer_spec_result(checker, id, &stack) } retained := 0 for spec in checker.specs { if spec.hir_id == hir.INVALID_FUNCTION { delete(spec.args, checker.allocator) delete(spec.comptime_values, checker.allocator) continue } checker.specs[retained] = spec checker.specs[retained].hir_id = hir.function_id(retained) retained += 1 } for len(checker.specs) > retained { _ = pop(&checker.specs) } } add_hir_expr :: proc(checker: ^Checker, expr: hir.Expr) -> hir.Expr_Id { id := hir.expr_id(len(checker.module.exprs)) append(&checker.module.exprs, expr) return id } invalid_hir_expr :: proc( checker: ^Checker, span: source.Span, diagnostic: source.Diagnostic_Id, recovery_type := types.INVALID, ) -> hir.Expr_Id { return add_hir_expr( checker, hir.Expr { kind = .Invalid, span = span, type = recovery_type, target = hir.INVALID_REF, left = hir.INVALID_EXPR, right = hir.INVALID_EXPR, diagnostic = diagnostic, }, ) } add_unique_global :: proc(values: ^[dynamic]hir.Global_Id, value: hir.Global_Id) { for existing in values { if existing == value { return } } append(values, value) } build_global_reference :: proc( checker: ^Checker, global: ast.Global_Id, span: source.Span, global_reads: ^[dynamic]hir.Global_Id, ) -> hir.Expr_Id { if int(global) < len(checker.external_global_diagnostics) { diagnostic := checker.external_global_diagnostics[global] if diagnostic != source.INVALID_DIAGNOSTIC { return invalid_hir_expr(checker, span, diagnostic, checker.global_types[global]) } } hir_global := hir.Global_Id(global) add_unique_global(global_reads, hir_global) return add_hir_expr(checker, hir.Expr{ kind=.Global, span=span, type=checker.global_types[global], target=hir.global_ref(hir_global), left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) } add_unique_function :: proc(values: ^[dynamic]hir.Function_Id, value: hir.Function_Id) { for existing in values { if existing == value { return } } append(values, value) } find_build_local :: proc(locals: []Build_Local, name: symbol.Id) -> (Build_Local, bool) { for index := len(locals) - 1; index >= 0; index -= 1 { if locals[index].name == name { return locals[index], true } } return Build_Local{}, false } can_implicitly_convert_type :: proc(checker: ^Checker, actual, expected: types.Type) -> bool { store := &checker.module.types if types.equal(actual, expected) || types.can_widen(actual, expected) || types.can_coerce_c_integer(actual, expected, checker.target) || types.can_coerce_c_scalar(actual, expected, checker.target) || types.can_weaken_pointer(actual, expected, store) || types.can_weaken_slice(actual, expected, store) || types.can_decay_slice_c_string(actual, expected, store) || types.can_decay_array_pointer(actual, expected, store) || types.can_sum_widen(actual, expected, store) { return true } if types.is_optional(expected, store) { return can_implicitly_convert_type(checker, actual, types.child_type(expected, store)) } return false } coerce_expr :: proc( checker: ^Checker, expr_id: hir.Expr_Id, expected: types.Type, span: source.Span, ) -> hir.Expr_Id { if expr_id == hir.INVALID_EXPR { return expr_id } actual := checker.module.exprs[expr_id].type if types.equal(actual, expected) { return expr_id } if types.can_weaken_pointer(actual, expected, &checker.module.types) { return add_hir_expr(checker, hir.Expr{ kind=.Weaken_Pointer, span=span, type=expected, left=expr_id, target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) } if types.can_weaken_slice(actual, expected, &checker.module.types) { return add_hir_expr(checker, hir.Expr{ kind=.Weaken_Slice, span=span, type=expected, left=expr_id, target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) } if types.can_decay_array_pointer(actual, expected, &checker.module.types) { return add_hir_expr(checker, hir.Expr{ kind=.Decay_Array_Pointer, span=span, type=expected, left=expr_id, target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) } if types.can_decay_slice_c_string(actual, expected, &checker.module.types) { return add_hir_expr(checker, hir.Expr{ kind=.Slice_Ptr, span=span, type=expected, left=expr_id, target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) } if types.can_sum_widen(actual, expected, &checker.module.types) { return add_hir_expr(checker, hir.Expr{ kind=.Sum_Widen, span=span, type=expected, left=expr_id, target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) } if types.is_optional(expected, &checker.module.types) { child := types.child_type(expected, &checker.module.types) if types.equal(actual, child) || types.can_widen(actual, child) || types.can_coerce_c_integer(actual, child, checker.target) || types.can_coerce_c_scalar(actual, child, checker.target) || types.can_weaken_pointer(actual, child, &checker.module.types) || types.can_weaken_slice(actual, child, &checker.module.types) || types.can_decay_slice_c_string(actual, child, &checker.module.types) || types.can_decay_array_pointer(actual, child, &checker.module.types) { value := coerce_expr(checker, expr_id, child, span) return add_hir_expr(checker, hir.Expr{ kind=.Optional_Some, span=span, type=expected, left=value, target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) } } if types.can_widen(actual, expected) { return add_hir_expr( checker, hir.Expr { kind = .Widen, span = span, type = expected, left = expr_id, target = hir.INVALID_REF, right = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC, }, ) } if types.can_coerce_c_integer(actual, expected, checker.target) || types.can_coerce_c_scalar(actual, expected, checker.target) { return add_hir_expr( checker, hir.Expr { kind = .C_Coerce, span = span, type = expected, left = expr_id, target = hir.INVALID_REF, right = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC, }, ) } id := source.addf( checker.diagnostics, span, "cannot implicitly convert %s to %s", type_label(checker, actual), type_label(checker, expected), ) return invalid_hir_expr(checker, span, id, expected) } promote_c_vararg_expr :: proc(checker: ^Checker, expr_id: hir.Expr_Id, span: source.Span) -> hir.Expr_Id { actual := checker.module.exprs[expr_id].type if !types.is_c_vararg_type(actual, &checker.module.types) { id := source.addf( checker.diagnostics, span, "C variadic argument must be a concrete scalar or pointer, got %s", types.name(actual), ) return invalid_hir_expr(checker, span, id, types.C_INT) } promoted := types.c_vararg_promotion(actual, checker.target, &checker.module.types) if types.equal(actual, promoted) { return expr_id } return add_hir_expr(checker, hir.Expr{ kind=.C_Vararg_Promote, span=span, type=promoted, left=expr_id, target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) } build_constant_expr :: proc( checker: ^Checker, expr: ast.Expr, constant: Constant, expected: types.Type, ) -> hir.Expr_Id { recovery_type := types.I64 if types.is_concrete_integer(expected) { recovery_type = expected } // An integer constant in a float context (e.g. `pi float = 3`) folds to a // float literal, mirroring build_float_expr's bit packing. if constant.kind == .Value && types.is_float(expected, checker.target) { fval := f64(constant.value) // ponytail: silent precision loss past 2^53, like C int->double bits := transmute(i64)fval if types.bits(expected, checker.target) == 32 { bits = i64(transmute(u32)f32(fval)) } return add_hir_expr(checker, hir.Expr{ kind = .Float, span = expr.span, type = expected, integer = bits, target = hir.INVALID_REF, left = hir.INVALID_EXPR, right = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC, }) } if constant.kind == .Div_By_Zero { id := source.add(checker.diagnostics, expr.span, "division by zero in constant expression") return invalid_hir_expr(checker, expr.span, id, recovery_type) } if constant.kind == .Overflow || (!types.is_concrete_integer(expected) && !fits_i64(constant.value)) { id := source.add( checker.diagnostics, expr.span, "integer constant expression exceeds signed i64 range", ) return invalid_hir_expr(checker, expr.span, id, recovery_type) } value := i64(constant.value) if constant.value >= 0 && constant.value <= i128(0xffff_ffff_ffff_ffff) { value = transmute(i64)u64(constant.value) } result_type := types.smallest_signed_for_literal(value) if types.is_concrete_integer(expected) { if !fits_integer_type(constant.value, expected, checker.target) { id := source.addf( checker.diagnostics, expr.span, "integer constant %d does not fit in %s", constant.value, types.name(expected), ) return invalid_hir_expr(checker, expr.span, id, expected) } result_type = expected } return add_hir_expr( checker, hir.Expr { kind = .Integer, span = expr.span, type = result_type, integer = value, target = hir.INVALID_REF, left = hir.INVALID_EXPR, right = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC, }, ) } build_float_expr :: proc(checker: ^Checker, expr: ast.Expr, expected: types.Type) -> hir.Expr_Id { result_type := types.F64 if types.is_float(expected, checker.target) { result_type = expected } else if types.is_valid(expected) { id := source.addf( checker.diagnostics, expr.span, "cannot implicitly convert f64 to %s", types.name(expected), ) return invalid_hir_expr(checker, expr.span, id, expected) } value := transmute(f64)expr.integer bits := transmute(i64)value if types.bits(result_type, checker.target) == 32 { bits = i64(transmute(u32)f32(value)) } return add_hir_expr(checker, hir.Expr{ kind=.Float, span=expr.span, type=result_type, integer=bits, target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) } Build_Expr_Frame :: struct { expr: ast.Expr_Id, expected: types.Type, target_type: types.Type, stage: u8, left: hir.Expr_Id, arg_index: int, arg_mode: Call_Argument_Mode, prefix: int, built_args: []hir.Expr_Id, arg_types: []types.Type, template: ast.Function_Id, } hir_location_writable :: proc(checker: ^Checker, expr_id: hir.Expr_Id, locals: []Build_Local) -> bool { if expr_id == hir.INVALID_EXPR || int(expr_id) >= len(checker.module.exprs) { return false } expr := checker.module.exprs[expr_id] #partial switch expr.kind { case .Local: id := hir.as_local(expr.target) for local in locals { if local.id == id { return local.mutable } } case .Global: id := hir.as_global(expr.target) return id != hir.INVALID_GLOBAL && int(id) < len(checker.module.globals) && checker.module.globals[id].writable case .Deref: pointer_type := checker.module.exprs[expr.left].type return types.is_mutable(pointer_type, &checker.module.types) case .Index: container_type := checker.module.exprs[expr.left].type item, ok := types.container(container_type, &checker.module.types) if !ok || !item.mutable { return false } if types.is_array(container_type, &checker.module.types) { return hir_location_writable(checker, expr.left, locals) } return true case .Field: base_type := checker.module.exprs[expr.left].type if types.is_pointer(base_type, &checker.module.types) { return types.is_mutable(base_type, &checker.module.types) } return hir_location_writable(checker, expr.left, locals) case: } return false } hir_is_location :: proc(checker: ^Checker, expr_id: hir.Expr_Id) -> bool { if expr_id == hir.INVALID_EXPR || int(expr_id) >= len(checker.module.exprs) { return false } #partial switch checker.module.exprs[expr_id].kind { case .Local, .Global, .Deref, .Index, .Field: return true } return false } sequence_item :: proc(value: types.Type, store: ^types.Store) -> (types.Node, bool) { item, ok := types.node(store, value) if ok && (item.kind == .Array || item.kind == .Slice) { return item, true } pointer, array, pointer_ok := types.array_pointer(value, store) if pointer_ok { array.mutable = pointer.mutable && array.mutable return array, true } return {}, false } find_struct_field :: proc(checker: ^Checker, struct_type: types.Type, name: symbol.Id) -> (int, types.Field, bool) { for field, index in types.fields_for(&checker.module.types, struct_type) { if field.name == u32(name) { return index, field, true } } return 0, {}, false } field_type_from_value :: proc(checker: ^Checker, expr: ast.Expr, base_type: types.Type) -> types.Type { store := &checker.module.types field_name := symbol_text(checker, expr.name) item, has_item := types.container(base_type, store) if has_item && (item.kind == .Array || item.kind == .Slice) { if field_name == "len" { return types.USIZE } if field_name == "ptr" && (item.kind == .Slice || types.is_pointer(base_type, store)) { return container_pointer_type(store, item) } } value_type := base_type if types.is_pointer(value_type, store) { value_type = types.child_type(value_type, store) } _, field, ok := find_struct_field(checker, value_type, expr.name) return field.type if ok else types.INVALID } infer_qualified_value_field_type :: proc( checker: ^Checker, expr: ast.Expr, locals: []Infer_Local, pkg: ast.Package_Id, file: ast.File_Id, ) -> (types.Type, bool) { if !symbol.is_valid(expr.qualifier) || find_import(checker, file, expr.qualifier) != ast.INVALID_IMPORT { return types.INVALID, false } base_type := find_infer_local(locals, expr.qualifier) if !types.is_valid(base_type) { if global := find_global(checker, expr.qualifier, pkg, file); global != ast.INVALID_GLOBAL { base_type = checker.global_types[global] } } if !types.is_valid(base_type) { return types.INVALID, false } return field_type_from_value(checker, expr, base_type), true } build_field_from_value :: proc( checker: ^Checker, expr: ast.Expr, base: hir.Expr_Id, base_type: types.Type, ) -> (hir.Expr_Id, bool) { store := &checker.module.types field_name := symbol_text(checker, expr.name) item, has_item := types.container(base_type, store) if has_item && (item.kind == .Array || item.kind == .Slice) { if field_name == "len" { return add_hir_expr(checker, hir.Expr{ kind=.Length, span=expr.span, type=types.USIZE, left=base, target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }), true } if field_name == "ptr" && (item.kind == .Slice || types.is_pointer(base_type, store)) { return add_hir_expr(checker, hir.Expr{ kind=.Slice_Ptr, span=expr.span, type=container_pointer_type(store, item), left=base, target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }), true } if field_name == "ptr" && item.kind == .Array { id := source.add(checker.diagnostics, expr.span, "arrays do not expose '.ptr'; take their address first") return invalid_hir_expr(checker, expr.span, id), false } } value_type := base_type if types.is_pointer(value_type, store) { value_type = types.child_type(value_type, store) } index, field, ok := find_struct_field(checker, value_type, expr.name) if !ok { id := source.addf(checker.diagnostics, expr.span, "unknown struct field '%s'", symbol_text(checker, expr.name)) return invalid_hir_expr(checker, expr.span, id), false } if types.is_void(field.type) { id := source.addf(checker.diagnostics, expr.span, "variant '%s' has no payload to read", symbol_text(checker, expr.name)) return invalid_hir_expr(checker, expr.span, id), false } return add_hir_expr(checker, hir.Expr{ kind=.Field, span=expr.span, type=field.type, integer=i64(index), left=base, target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }), true } build_qualified_value_field :: proc( checker: ^Checker, expr: ast.Expr, locals: []Build_Local, global_reads: ^[dynamic]hir.Global_Id, pkg: ast.Package_Id, file: ast.File_Id, ) -> (hir.Expr_Id, bool, bool) { if !symbol.is_valid(expr.qualifier) || find_import(checker, file, expr.qualifier, true) != ast.INVALID_IMPORT { return hir.INVALID_EXPR, false, false } if local, ok := find_build_local(locals, expr.qualifier); ok { base := build_local_expr(checker, local, expr.span) value, ok := build_field_from_value(checker, expr, base, local.type) return value, true, ok } if global := find_global(checker, expr.qualifier, pkg, file); global != ast.INVALID_GLOBAL { base := build_global_reference(checker, global, expr.span, global_reads) value, ok := build_field_from_value(checker, expr, base, checker.global_types[global]) return value, true, ok } return hir.INVALID_EXPR, false, false } find_enum_member :: proc(checker: ^Checker, enum_type: types.Type, name: symbol.Id) -> (types.Enum_Member, bool) { for member in types.enum_members_for(&checker.module.types, enum_type) { if member.name == u32(name) { return member, true } } return {}, false } enum_type_from_name_expr :: proc( checker: ^Checker, expr: ast.Expr, pkg: ast.Package_Id, file: ast.File_Id, ) -> (types.Type, bool) { if !symbol.is_valid(expr.qualifier) || find_import(checker, file, expr.qualifier) != ast.INVALID_IMPORT { return types.INVALID, false } enum_type := types.find_named(&checker.module.types, u32(pkg), u32(expr.qualifier), file=u32(file)) return enum_type, types.is_enum(enum_type, &checker.module.types) } enum_type_from_field_expr :: proc( checker: ^Checker, expr: ast.Expr, pkg: ast.Package_Id, file: ast.File_Id, mark_used := false, ) -> (types.Type, bool) { if expr.left == ast.INVALID_EXPR || int(expr.left) >= len(checker.ast_module.exprs) { return types.INVALID, false } base := checker.ast_module.exprs[expr.left] if base.kind != .Name || !symbol.is_valid(base.qualifier) { return types.INVALID, false } target_pkg, available := expr_package(checker, base, pkg, file, mark_used) if !available { return types.INVALID, false } enum_type := types.find_named(&checker.module.types, u32(target_pkg), u32(base.name), file=u32(expr_lookup_file(base, file))) return enum_type, types.is_enum(enum_type, &checker.module.types) } enum_member_hir :: proc( checker: ^Checker, enum_type: types.Type, name: symbol.Id, span: source.Span, ) -> hir.Expr_Id { member, ok := find_enum_member(checker, enum_type, name) if !ok { id := source.addf(checker.diagnostics, span, "unknown enum member '%s'", symbol_text(checker, name)) return invalid_hir_expr(checker, span, id, enum_type) } value := i64(member.value) if member.value < 0 else transmute(i64)u64(member.value) return add_hir_expr(checker, hir.Expr{ kind=.Integer, span=span, type=enum_type, integer=value, target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) } build_function_value :: proc( checker: ^Checker, template: ast.Function_Id, span: source.Span, expected: types.Type, ) -> hir.Expr_Id { if template == ast.INVALID_FUNCTION || int(template) >= len(checker.ast_module.functions) { return hir.INVALID_EXPR } function := checker.ast_module.functions[template] if len(function.unsupported_reason) > 0 { id := source.addf( checker.diagnostics, span, "C declaration '%s' is unavailable: %s", symbol_text(checker, function.name), function.unsupported_reason, ) return invalid_hir_expr(checker, span, id) } if checker.template_diagnostics[template] != source.INVALID_DIAGNOSTIC { return invalid_hir_expr(checker, span, checker.template_diagnostics[template]) } params, result, ok := function_value_signature(checker, template) if !ok { id := source.addf( checker.diagnostics, span, "function '%s' cannot be used as a function value; expected a concrete non-comptime signature", symbol_text(checker, function.name), ) return invalid_hir_expr(checker, span, id) } defer delete(params, checker.allocator) function_type := types.function(&checker.module.types, params, result, function.c_abi, function.variadic) pointer_type := types.pointer(&checker.module.types, function_type, false, false) expected_pointer := expected if types.is_optional(expected_pointer, &checker.module.types) { expected_pointer = types.child_type(expected_pointer, &checker.module.types) } if _, _, expected_function, ok := types.function_pointer(expected_pointer, &checker.module.types); ok && types.equal(expected_function, function_type) { pointer_type = expected_pointer } spec := find_spec(checker, template, params) if spec == INVALID_SPEC { id := source.addf( checker.diagnostics, span, "could not resolve callback specialization of '%s'", symbol_text(checker, function.name), ) return invalid_hir_expr(checker, span, id, pointer_type) } function_id := checker.specs[spec].hir_id assert(function_id != hir.INVALID_FUNCTION) return add_hir_expr(checker, hir.Expr{ kind=.Function, span=span, type=pointer_type, target=hir.function_ref(function_id), left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) } build_nested_expr :: proc( checker: ^Checker, expr_id: ast.Expr_Id, locals: []Build_Local, global_reads: ^[dynamic]hir.Global_Id, calls: ^[dynamic]hir.Function_Id, expected: types.Type, pkg: ast.Package_Id, file: ast.File_Id, ) -> hir.Expr_Id { outer := checker.build_stack checker.build_stack = nil checker.build_stack.allocator = checker.allocator result := build_expr(checker, expr_id, locals, global_reads, calls, expected, pkg, file) delete(checker.build_stack) checker.build_stack = outer return result } fallible_aggregate :: proc( checker: ^Checker, span: source.Span, channel: types.Type, value: hir.Expr_Id, error_path: bool, ) -> hir.Expr_Id { values := make([]hir.Expr_Id, 1, checker.allocator) values[0] = value return add_hir_expr(checker, hir.Expr{ kind=.Struct, span=span, type=channel, integer=1 if error_path else 0, args=values, target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) } build_compound_expr :: proc( checker: ^Checker, expr: ast.Expr, locals: []Build_Local, global_reads: ^[dynamic]hir.Global_Id, calls: ^[dynamic]hir.Function_Id, expected: types.Type, pkg: ast.Package_Id, file: ast.File_Id, ) -> hir.Expr_Id { store := &checker.module.types #partial switch expr.kind { case .String: string_type := string_literal_type(checker, expr.integer) return add_hir_expr(checker, hir.Expr{ kind=.String, span=expr.span, type=string_type, integer=i64(expr.integer), target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) case .Array: element_type := types.INVALID result_type := expected expected_node, has_expected := types.node(store, expected) if !has_expected || expected_node.kind != .Array { has_expected = false result_type = types.INVALID } else { element_type = expected_node.child if !expected_node.inferred_count && expected_node.count != u64(len(expr.args)) { id := source.addf( checker.diagnostics, expr.span, "array literal expects %d elements, got %d", expected_node.count, len(expr.args), ) return invalid_hir_expr(checker, expr.span, id, expected) } if expected_node.inferred_count { result_type = types.with_array_count(store, expected, u64(len(expr.args))) } } if !has_expected { infer_locals := make([]Infer_Local, len(locals), checker.allocator) defer delete(infer_locals, checker.allocator) for local, index in locals { infer_locals[index] = Infer_Local{name=local.name, type=local.type, declared=local.type, statement=ast.INVALID_STMT} } for arg in expr.args { actual := infer_nested_expr(checker, arg, infer_locals, pkg, file, nil) if !types.is_valid(element_type) { element_type = actual } else { element_type = types.widest(element_type, actual) } } if !types.is_valid(element_type) { element_type = types.I64 } result_type = types.array(store, element_type, u64(len(expr.args)), false) } args := make([]hir.Expr_Id, len(expr.args), checker.allocator) for arg, index in expr.args { args[index] = build_nested_expr( checker, arg, locals, global_reads, calls, element_type, pkg, file, ) args[index] = coerce_expr(checker, args[index], element_type, checker.module.exprs[args[index]].span) } return add_hir_expr(checker, hir.Expr{ kind=.Array, span=expr.span, type=result_type, args=args, target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) case .None: if !types.is_optional(expected, store) { id := source.add(checker.diagnostics, expr.span, "'none' requires an optional context") return invalid_hir_expr(checker, expr.span, id, expected) } return add_hir_expr(checker, hir.Expr{ kind=.None, span=expr.span, type=expected, target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) case .Undefined: id := source.add( checker.diagnostics, expr.span, "'undefined' is only valid as a mutable local declaration initializer", ) return invalid_hir_expr(checker, expr.span, id, expected) case .Enum_Literal: if types.is_tagged_union(expected, store) { index, field, found := find_struct_field(checker, expected, expr.name) if !found { id := source.addf(checker.diagnostics, expr.span, "unknown variant '.%s' on '%s'", symbol_text(checker, expr.name), type_label(checker, expected)) return invalid_hir_expr(checker, expr.span, id, expected) } values := make([]hir.Expr_Id, 1, checker.allocator) if expr.left == ast.INVALID_EXPR { if !types.is_void(field.type) { id := source.addf(checker.diagnostics, expr.span, "variant '.%s' on '%s' needs a payload; only void variants can be built from a bare '.%s'", symbol_text(checker, expr.name), type_label(checker, expected), symbol_text(checker, expr.name)) delete(values, checker.allocator) return invalid_hir_expr(checker, expr.span, id, expected) } values[0] = hir.INVALID_EXPR } else { if types.is_void(field.type) { id := source.addf(checker.diagnostics, expr.span, "void variant '%s' takes no value", symbol_text(checker, expr.name)) delete(values, checker.allocator) return invalid_hir_expr(checker, expr.span, id, expected) } values[0] = build_nested_expr(checker, expr.left, locals, global_reads, calls, field.type, pkg, file) values[0] = coerce_expr(checker, values[0], field.type, checker.module.exprs[values[0]].span) } return add_hir_expr(checker, hir.Expr{ kind=.Struct, span=expr.span, type=expected, args=values, integer=i64(index), target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) } if expr.left != ast.INVALID_EXPR { id := source.addf( checker.diagnostics, expr.span, "'.%s{...}' requires a tagged-union context", symbol_text(checker, expr.name), ) return invalid_hir_expr(checker, expr.span, id, expected) } if !types.is_enum(expected, store) { id := source.addf( checker.diagnostics, expr.span, "'.%s' requires an enum context", symbol_text(checker, expr.name), ) return invalid_hir_expr(checker, expr.span, id, expected) } return enum_member_hir(checker, expected, expr.name, expr.span) case .Cast: target := type_from_syntax(checker, expr.type, pkg, file) value := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file) actual := checker.module.exprs[value].type valid_target := types.is_concrete_scalar(target) && !types.is_bool(target) valid_actual := types.is_concrete_scalar(actual) && !types.is_bool(actual) if !valid_target || !valid_actual { id := source.addf( checker.diagnostics, expr.span, "scalar cast requires numeric scalar types, got %s to %s", types.name(actual), types.name(target), ) return invalid_hir_expr(checker, expr.span, id, target) } return add_hir_expr(checker, hir.Expr{ kind=.Scalar_Cast, span=expr.span, type=target, left=value, target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) case .Address: // `&` (Zig's `&.{...}`): the operand is an rvalue with no // address, so promote it to an anonymous global constant and take *its* // address. Reuses the existing non-scalar-global storage path; only the // stable global address enters the expression, so it never dangles. The // resulting `*[N]T` then decays to a slice via the usual coercion. if expr.left != ast.INVALID_EXPR && checker.ast_module.exprs[expr.left].kind == .Array { operand := checker.ast_module.exprs[expr.left] // Propagate an element-expected type through `&` so literal elements // coerce to the target slice's element type (e.g. string -> []u8). // Without this, `&["x"]` infers `*[1]*[N:0]u8`, which won't decay to // `[][]u8` because can_decay_array_pointer requires child equality. element := types.INVALID if node, ok := types.node(store, expected); ok && (node.kind == .Slice || node.kind == .Array) { element = node.child } synth_expected := types.INVALID if types.is_valid(element) { synth_expected = types.array(store, element, u64(len(operand.args)), false) } value := build_nested_expr(checker, expr.left, locals, global_reads, calls, synth_expected, pkg, file) array_type := checker.module.exprs[value].type hidden_id := hir.Global_Id(len(checker.ast_module.globals) + len(checker.anon_globals)) append(&checker.anon_globals, hir.Global{ name = symbol.intern(checker.symbols, "__anon.array"), type = array_type, expr = value, writable = false, external = false, diagnostic = source.INVALID_DIAGNOSTIC, }) add_unique_global(global_reads, hidden_id) global_ref := add_hir_expr(checker, hir.Expr{ kind=.Global, span=expr.span, type=array_type, target=hir.global_ref(hidden_id), left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) return add_hir_expr(checker, hir.Expr{ kind=.Address, span=expr.span, type=types.pointer(store, array_type, false, false), left=global_ref, target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) } value := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file) if !hir_is_location(checker, value) { id := source.add(checker.diagnostics, expr.span, "'&' requires an addressable location") return invalid_hir_expr(checker, expr.span, id) } value_type := checker.module.exprs[value].type writable := hir_location_writable(checker, value, locals) result_type := types.pointer(store, value_type, writable, false) if types.is_pointer(expected, store) && types.equal(types.child_type(expected, store), value_type) && (!types.is_mutable(expected, store) || writable) { result_type = expected } return add_hir_expr(checker, hir.Expr{ kind=.Address, span=expr.span, type=result_type, left=value, target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) case .Deref: pointer := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file) pointer_type := checker.module.exprs[pointer].type if !types.is_pointer(pointer_type, store) { id := source.add(checker.diagnostics, expr.span, "postfix '^' requires a pointer") return invalid_hir_expr(checker, expr.span, id) } return add_hir_expr(checker, hir.Expr{ kind=.Deref, span=expr.span, type=types.child_type(pointer_type, store), left=pointer, target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) case .Index: container := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file) index := build_nested_expr(checker, expr.right, locals, global_reads, calls, types.USIZE, pkg, file) index = coerce_expr(checker, index, types.USIZE, expr.span) container_type := checker.module.exprs[container].type item, ok := types.container(container_type, store) if !ok { id := source.add(checker.diagnostics, expr.span, "indexing requires an array, slice, pointer-to-array, or many-item pointer") return invalid_hir_expr(checker, expr.span, id) } return add_hir_expr(checker, hir.Expr{ kind=.Index, span=expr.span, type=item.child, left=container, right=index, target=hir.INVALID_REF, diagnostic=source.INVALID_DIAGNOSTIC, }) case .Slice: container := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file) container_type := checker.module.exprs[container].type item, ok := types.container(container_type, store) if !ok { id := source.add(checker.diagnostics, expr.span, "slicing requires an array, slice, pointer-to-array, or many-item pointer") return invalid_hir_expr(checker, expr.span, id) } if item.kind == .Pointer && expr.args[1] == ast.INVALID_EXPR { id := source.add(checker.diagnostics, expr.span, "many-item pointer slicing requires an explicit end bound") return invalid_hir_expr(checker, expr.span, id) } bounds := make([]hir.Expr_Id, 2, checker.allocator) bounds[0] = hir.INVALID_EXPR bounds[1] = hir.INVALID_EXPR for bound, index in expr.args { if bound != ast.INVALID_EXPR { bounds[index] = build_nested_expr(checker, bound, locals, global_reads, calls, types.USIZE, pkg, file) bounds[index] = coerce_expr(checker, bounds[index], types.USIZE, checker.ast_module.exprs[bound].span) } } preserve_sentinel := item.has_sentinel && expr.args[1] == ast.INVALID_EXPR result_type := types.slice(store, item.child, item.mutable, preserve_sentinel, item.sentinel) return add_hir_expr(checker, hir.Expr{ kind=.Slice, span=expr.span, type=result_type, args=bounds, left=container, target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) case .Field: if enum_type, enum_ok := enum_type_from_field_expr(checker, expr, pkg, file, true); enum_ok { return enum_member_hir(checker, enum_type, expr.name, expr.span) } base := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file) base_type := checker.module.exprs[base].type field_name := symbol_text(checker, expr.name) item, has_item := types.container(base_type, store) if has_item && (item.kind == .Array || item.kind == .Slice) { if field_name == "len" { return add_hir_expr(checker, hir.Expr{ kind=.Length, span=expr.span, type=types.USIZE, left=base, target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) } if field_name == "ptr" && (item.kind == .Slice || types.is_pointer(base_type, store)) { return add_hir_expr(checker, hir.Expr{ kind=.Slice_Ptr, span=expr.span, type=container_pointer_type(store, item), left=base, target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) } if field_name == "ptr" && item.kind == .Array { id := source.add(checker.diagnostics, expr.span, "arrays do not expose '.ptr'; take their address first") return invalid_hir_expr(checker, expr.span, id) } } if types.is_pointer(base_type, store) { base_type = types.child_type(base_type, store) } index, field, ok := find_struct_field(checker, base_type, expr.name) if !ok { id := source.addf(checker.diagnostics, expr.span, "unknown struct field '%s'", symbol_text(checker, expr.name)) return invalid_hir_expr(checker, expr.span, id) } if types.is_void(field.type) { id := source.addf(checker.diagnostics, expr.span, "variant '%s' has no payload to read", symbol_text(checker, expr.name)) return invalid_hir_expr(checker, expr.span, id) } return add_hir_expr(checker, hir.Expr{ kind=.Field, span=expr.span, type=field.type, integer=i64(index), left=base, target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) case .Unwrap: optional := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file) optional_type := checker.module.exprs[optional].type if !types.is_optional(optional_type, store) { id := source.add(checker.diagnostics, expr.span, "postfix '?' requires an optional") return invalid_hir_expr(checker, expr.span, id) } return add_hir_expr(checker, hir.Expr{ kind=.Unwrap, span=expr.span, type=types.child_type(optional_type, store), left=optional, target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) case .Orelse: optional := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file) optional_type := checker.module.exprs[optional].type if !types.is_optional(optional_type, store) { id := source.add(checker.diagnostics, expr.span, "'orelse' requires an optional left operand") return invalid_hir_expr(checker, expr.span, id) } child := types.child_type(optional_type, store) fallback := build_nested_expr(checker, expr.right, locals, global_reads, calls, child, pkg, file) fallback = coerce_expr(checker, fallback, child, checker.module.exprs[fallback].span) return add_hir_expr(checker, hir.Expr{ kind=.Orelse, span=expr.span, type=child, left=optional, right=fallback, target=hir.INVALID_REF, diagnostic=source.INVALID_DIAGNOSTIC, }) case .Try: left_expected := expected if checker.ast_module.exprs[expr.left].kind == .Call else types.INVALID channel := build_nested_expr(checker, expr.left, locals, global_reads, calls, left_expected, pkg, file) channel_type := checker.module.exprs[channel].type success := types.fallible_success(channel_type, store) if !types.is_valid(success) { id := source.add(checker.diagnostics, expr.span, "'try' requires a fallible expression") return invalid_hir_expr(checker, expr.span, id) } enclosing_success := types.fallible_success(checker.current_result, store) enclosing_error := types.fallible_error(checker.current_result, store) if !types.is_valid(enclosing_success) { id := source.add(checker.diagnostics, expr.span, "'try' requires an enclosing fallible function") return invalid_hir_expr(checker, expr.span, id, success) } if !types.equal(success, enclosing_success) { id := source.add(checker.diagnostics, expr.span, "'try' success type must match the enclosing fallible result") return invalid_hir_expr(checker, expr.span, id, success) } error_type := types.fallible_error(channel_type, store) if !types.equal(error_type, enclosing_error) && !types.can_sum_widen(error_type, enclosing_error, store) { id := source.add(checker.diagnostics, expr.span, "'try' error channel cannot be widened to the enclosing error channel") return invalid_hir_expr(checker, expr.span, id, success) } return add_hir_expr(checker, hir.Expr{ kind=.Try, span=expr.span, type=success, left=channel, target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) case .Catch: left_expected := expected if checker.ast_module.exprs[expr.left].kind == .Call else types.INVALID channel := build_nested_expr(checker, expr.left, locals, global_reads, calls, left_expected, pkg, file) channel_type := checker.module.exprs[channel].type success := types.fallible_success(channel_type, store) if !types.is_valid(success) { id := source.add(checker.diagnostics, expr.span, "'catch' requires a fallible expression") return invalid_hir_expr(checker, expr.span, id) } body: []hir.Stmt_Id capture := hir.INVALID_LOCAL block_handler := false fallback := hir.INVALID_EXPR if expr.right != ast.INVALID_EXPR { fallback = build_nested_expr(checker, expr.right, locals, global_reads, calls, success, pkg, file) fallback = coerce_expr(checker, fallback, success, checker.module.exprs[fallback].span) } else { block_handler = true ctx := checker.current_build_ctx if ctx == nil { id := source.add(checker.diagnostics, expr.span, "catch block form is only valid in a function body") return invalid_hir_expr(checker, expr.span, id, success) } capture_start := len(ctx.locals^) error_type := types.fallible_error(channel_type, store) if symbol.is_valid(expr.name) && expr.name != checker.sink_symbol { capture = append_build_local(ctx, expr.name, error_type, false, expr.span) } handler: [dynamic]hir.Stmt_Id handler.allocator = checker.allocator fallback, _ = build_value_source(ctx, &handler, expr.body, success, expr.span, allow_exit=true) body = handler[:] resize(ctx.locals, capture_start) } return add_hir_expr(checker, hir.Expr{ kind=.Catch, span=expr.span, type=success, integer=1 if block_handler else 0, left=channel, right=fallback, body=body, target=hir.local_ref(capture) if capture != hir.INVALID_LOCAL else hir.INVALID_REF, diagnostic=source.INVALID_DIAGNOSTIC, }) case .Range: expected_child := types.INVALID if types.is_range(expected, store) { expected_child = types.child_type(expected, store) } left_const := eval_constant(checker, expr.left) right_const := eval_constant(checker, expr.right) left, right: hir.Expr_Id if types.is_valid(expected_child) { left = build_nested_expr(checker, expr.left, locals, global_reads, calls, expected_child, pkg, file) right = build_nested_expr(checker, expr.right, locals, global_reads, calls, expected_child, pkg, file) } else if right_const.kind == .Value && left_const.kind != .Value { left = build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file) right = build_nested_expr(checker, expr.right, locals, global_reads, calls, checker.module.exprs[left].type, pkg, file) } else if left_const.kind == .Value && right_const.kind != .Value { right = build_nested_expr(checker, expr.right, locals, global_reads, calls, types.INVALID, pkg, file) left = build_nested_expr(checker, expr.left, locals, global_reads, calls, checker.module.exprs[right].type, pkg, file) } else { left = build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file) right = build_nested_expr(checker, expr.right, locals, global_reads, calls, types.INVALID, pkg, file) } child := expected_child if !types.is_valid(child) { child = types.widest(checker.module.exprs[left].type, checker.module.exprs[right].type) } if !types.is_concrete_integer(child) { id := source.add(checker.diagnostics, expr.span, "range bounds must be compatible concrete integers") return invalid_hir_expr(checker, expr.span, id) } left = coerce_expr(checker, left, child, checker.module.exprs[left].span) right = coerce_expr(checker, right, child, checker.module.exprs[right].span) args := make([]hir.Expr_Id, 2, checker.allocator) args[0] = left args[1] = right return add_hir_expr(checker, hir.Expr{ kind=.Range, span=expr.span, type=types.range(store, child), integer=i64(expr.integer), args=args, target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) case .Comptime: return build_comptime_expr(checker, expr, expected, pkg, file) case .Bool: return add_hir_expr(checker, hir.Expr{ kind=.Bool, span=expr.span, type=types.BOOL, integer=i64(expr.integer), target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) case .Not: operand := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.BOOL, pkg, file) operand_type := checker.module.exprs[operand].type if checker.module.exprs[operand].kind != .Invalid && !types.is_bool(operand_type) { id := source.add(checker.diagnostics, expr.span, "'!' requires a bool operand") return invalid_hir_expr(checker, expr.span, id, types.BOOL) } return add_hir_expr(checker, hir.Expr{ kind=.Not, span=expr.span, type=types.BOOL, left=operand, target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) case .And, .Or: left := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.BOOL, pkg, file) right := build_nested_expr(checker, expr.right, locals, global_reads, calls, types.BOOL, pkg, file) left_type := checker.module.exprs[left].type right_type := checker.module.exprs[right].type left_ok := checker.module.exprs[left].kind == .Invalid || types.is_bool(left_type) right_ok := checker.module.exprs[right].kind == .Invalid || types.is_bool(right_type) if !left_ok || !right_ok { id := source.add(checker.diagnostics, expr.span, "'and'/'or' require bool operands") return invalid_hir_expr(checker, expr.span, id, types.BOOL) } return add_hir_expr(checker, hir.Expr{ kind=.And if expr.kind == .And else .Or, span=expr.span, type=types.BOOL, left=left, right=right, target=hir.INVALID_REF, diagnostic=source.INVALID_DIAGNOSTIC, }) case .Eq, .Ne, .Lt, .Le, .Gt, .Ge: // Contextualize a bare integer-literal operand to the other operand's type // so comparisons like `count > 0` or `0 < count` type-check. left_const := eval_constant(checker, expr.left) right_const := eval_constant(checker, expr.right) left, right: hir.Expr_Id left_expr := checker.ast_module.exprs[expr.left] right_expr := checker.ast_module.exprs[expr.right] left_numeric_const := left_const.kind == .Value || is_float_constant_expr(checker, expr.left) right_numeric_const := right_const.kind == .Value || is_float_constant_expr(checker, expr.right) if right_expr.kind == .Enum_Literal && left_expr.kind != .Enum_Literal { left = build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file) right = build_nested_expr(checker, expr.right, locals, global_reads, calls, checker.module.exprs[left].type, pkg, file) } else if left_expr.kind == .Enum_Literal && right_expr.kind != .Enum_Literal { right = build_nested_expr(checker, expr.right, locals, global_reads, calls, types.INVALID, pkg, file) left = build_nested_expr(checker, expr.left, locals, global_reads, calls, checker.module.exprs[right].type, pkg, file) } else if right_numeric_const && !left_numeric_const { left = build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file) hint := checker.module.exprs[left].type right = build_nested_expr(checker, expr.right, locals, global_reads, calls, hint, pkg, file) } else if left_numeric_const && !right_numeric_const { right = build_nested_expr(checker, expr.right, locals, global_reads, calls, types.INVALID, pkg, file) hint := checker.module.exprs[right].type left = build_nested_expr(checker, expr.left, locals, global_reads, calls, hint, pkg, file) } else { left = build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file) right = build_nested_expr(checker, expr.right, locals, global_reads, calls, types.INVALID, pkg, file) } left_type := checker.module.exprs[left].type right_type := checker.module.exprs[right].type if checker.module.exprs[left].kind == .Invalid || checker.module.exprs[right].kind == .Invalid { return invalid_hir_expr(checker, expr.span, expr.diagnostic, types.BOOL) } operand_type := types.INVALID if types.is_enum(left_type, store) || types.is_enum(right_type, store) { if !types.equal(left_type, right_type) || (expr.kind != .Eq && expr.kind != .Ne) { id := source.add(checker.diagnostics, expr.span, "enum values only support '==' and '!=' with the same enum type") return invalid_hir_expr(checker, expr.span, id, types.BOOL) } operand_type = left_type } else if types.is_bool(left_type) && types.is_bool(right_type) { if expr.kind != .Eq && expr.kind != .Ne { id := source.add(checker.diagnostics, expr.span, "bool values only support '==' and '!='") return invalid_hir_expr(checker, expr.span, id, types.BOOL) } operand_type = types.BOOL } else { operand_type = types.widest(left_type, right_type) if !types.is_concrete_scalar(operand_type) || types.is_bool(operand_type) { id := source.add(checker.diagnostics, expr.span, "comparison requires compatible numeric operands") return invalid_hir_expr(checker, expr.span, id, types.BOOL) } } left = coerce_expr(checker, left, operand_type, checker.module.exprs[left].span) right = coerce_expr(checker, right, operand_type, checker.module.exprs[right].span) compare_kind := hir.Expr_Kind.Eq #partial switch expr.kind { case .Eq: compare_kind = .Eq case .Ne: compare_kind = .Ne case .Lt: compare_kind = .Lt case .Le: compare_kind = .Le case .Gt: compare_kind = .Gt case .Ge: compare_kind = .Ge } return add_hir_expr(checker, hir.Expr{ kind=compare_kind, span=expr.span, type=types.BOOL, left=left, right=right, target=hir.INVALID_REF, diagnostic=source.INVALID_DIAGNOSTIC, }) case .Struct_Literal: struct_type := types.INVALID if expr.left != ast.INVALID_EXPR { struct_type, _ = resolve_type_argument(checker, expr.left, pkg, file) struct_type = types.resolve_alias(struct_type, store) } else if symbol.is_valid(expr.name) { target_pkg, available := expr_package(checker, expr, pkg, file, true) struct_type = types.find_named(store, u32(target_pkg), u32(expr.name), file=u32(expr_lookup_file(expr, file))) if available else types.INVALID struct_type = types.resolve_alias(struct_type, store) if !types.is_record(struct_type, store) || types.is_opaque_struct(struct_type, store) { id := source.addf(checker.diagnostics, expr.span, "unknown or opaque record type '%s'", symbol_text(checker, expr.name)) return invalid_hir_expr(checker, expr.span, id) } } else { struct_type = types.resolve_alias(expected, store) if !types.is_struct(struct_type, store) || types.is_opaque_struct(struct_type, store) { id := source.add(checker.diagnostics, expr.span, "keyed contextual payload requires a struct payload") return invalid_hir_expr(checker, expr.span, id, expected) } } fields := types.fields_for(store, struct_type) union_record := types.is_union(struct_type, store) if union_record && len(expr.args) != 1 { id := source.add(checker.diagnostics, expr.span, "union literal requires exactly one field initializer") return invalid_hir_expr(checker, expr.span, id, struct_type) } values := make([]hir.Expr_Id, 1 if union_record else len(fields), checker.allocator) initialized := make([]bool, len(fields), checker.allocator) defer delete(initialized, checker.allocator) for &value in values { value = hir.INVALID_EXPR } for keyed in expr.args { keyed_expr := checker.ast_module.exprs[keyed] index, field, ok := find_struct_field(checker, struct_type, keyed_expr.name) if !ok { source.addf(checker.diagnostics, keyed_expr.span, "unknown struct field '%s'", symbol_text(checker, keyed_expr.name)) continue } if initialized[index] { source.addf(checker.diagnostics, keyed_expr.span, "duplicate initializer for struct field '%s'", symbol_text(checker, keyed_expr.name)) continue } initialized[index] = true value_index := 0 if union_record else index is_void_field := union_record && types.is_void(field.type) if keyed_expr.left == ast.INVALID_EXPR { // Bare key `T{ variant }`: valid only to construct a void-payload // variant (no value); the payload slot stays INVALID_EXPR. if !is_void_field { source.addf(checker.diagnostics, keyed_expr.span, "field '%s' requires a value", symbol_text(checker, keyed_expr.name)) } } else { if is_void_field { source.addf(checker.diagnostics, keyed_expr.span, "void variant '%s' takes no value", symbol_text(checker, keyed_expr.name)) } values[value_index] = build_nested_expr(checker, keyed_expr.left, locals, global_reads, calls, field.type, pkg, file) values[value_index] = coerce_expr(checker, values[value_index], field.type, keyed_expr.span) } } if !union_record { for field, index in fields { if values[index] != hir.INVALID_EXPR { continue } id := source.addf(checker.diagnostics, expr.span, "missing initializer for struct field '%s'", symbol_text(checker, symbol.Id(field.name))) delete(values, checker.allocator) return invalid_hir_expr(checker, expr.span, id, struct_type) } } active_field: i64 if union_record { // The active variant is the one initialized field. Its payload slot // (`values[0]`) may legitimately be INVALID_EXPR for a void variant, so // detect "no field" via `initialized`, not the payload value. found_any := false for value, index in initialized { if value { active_field = i64(index) found_any = true break } } if !found_any { delete(values, checker.allocator) return invalid_hir_expr(checker, expr.span, source.add(checker.diagnostics, expr.span, "union literal requires a known field"), struct_type) } } return add_hir_expr(checker, hir.Expr{ kind=.Struct, span=expr.span, type=struct_type, args=values, integer=active_field, target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) case .Keyed: id := source.add(checker.diagnostics, expr.span, "keyed initializer is only valid inside a struct literal") return invalid_hir_expr(checker, expr.span, id) case: return invalid_hir_expr(checker, expr.span, expr.diagnostic) } } // build_binary_arith constructs the HIR node for `left op right`, where `op` is // an arithmetic AST kind (`Add`/`Sub`/`Mul`/`Div`). It models many-pointer `+` // as `Pointer_Add`, coerces both operands to their common type, and emits the // "arithmetic requires compatible numeric operands" diagnostic when they have no // shared numeric type. build_binary_arith :: proc( checker: ^Checker, op: ast.Expr_Kind, left, right: hir.Expr_Id, span: source.Span, ) -> hir.Expr_Id { // Pointer arithmetic is only defined for `+` (many-pointer + usize). if op == .Add && types.is_many_pointer(checker.module.exprs[left].type, &checker.module.types) && types.equal(checker.module.exprs[right].type, types.USIZE) { return add_hir_expr(checker, hir.Expr{ kind=.Pointer_Add, span=span, type=checker.module.exprs[left].type, left=left, right=right, target=hir.INVALID_REF, diagnostic=source.INVALID_DIAGNOSTIC, }) } result := types.widest(checker.module.exprs[left].type, checker.module.exprs[right].type) if !types.is_concrete_scalar(result) { id := source.add(checker.diagnostics, span, "arithmetic requires compatible numeric operands") return invalid_hir_expr(checker, span, id) } result_kind := hir.Expr_Kind.Add #partial switch op { case .Sub: result_kind = .Sub case .Mul: result_kind = .Mul case .Div: result_kind = .Div } coerced_left := coerce_expr(checker, left, result, checker.module.exprs[left].span) coerced_right := coerce_expr(checker, right, result, checker.module.exprs[right].span) return add_hir_expr(checker, hir.Expr{ kind=result_kind, span=span, type=result, left=coerced_left, right=coerced_right, target=hir.INVALID_REF, diagnostic=source.INVALID_DIAGNOSTIC, }) } build_expr :: proc( checker: ^Checker, expr_id: ast.Expr_Id, locals: []Build_Local, global_reads: ^[dynamic]hir.Global_Id, calls: ^[dynamic]hir.Function_Id, expected := types.INVALID, pkg := ast.Package_Id(0), file := ast.File_Id(0), ) -> hir.Expr_Id { stack := checker.build_stack clear_dynamic_array(&stack) defer { for frame in stack { delete(frame.built_args, checker.allocator) delete(frame.arg_types, checker.allocator) } clear_dynamic_array(&stack) checker.build_stack = stack } append(&stack, Build_Expr_Frame{expr=expr_id, expected=expected, template=ast.INVALID_FUNCTION}) last := hir.INVALID_EXPR for len(stack) > 0 { frame_index := len(stack)-1 frame := stack[frame_index] if frame.expr == ast.INVALID_EXPR || int(frame.expr) >= len(checker.ast_module.exprs) { id := source.add(checker.diagnostics, source.Span{}, "missing expression") last = invalid_hir_expr(checker, source.Span{}, id) _ = pop(&stack) continue } expr := checker.ast_module.exprs[frame.expr] if frame.stage == 0 { constant := eval_constant(checker, frame.expr) if constant.kind == .Value || constant.kind == .Overflow || constant.kind == .Div_By_Zero { last = build_constant_expr(checker, expr, constant, frame.expected) _ = pop(&stack) continue } switch expr.kind { case .String, .Array, .None, .Undefined, .Address, .Deref, .Index, .Slice, .Field, .Unwrap, .Orelse, .Try, .Catch, .Struct_Literal, .Keyed, .Bool, .Cast, .Comptime, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range, .Enum_Literal: last = build_compound_expr( checker, expr, locals, global_reads, calls, frame.expected, pkg, file, ) _ = pop(&stack) case .Function_Literal: template := ast.Function_Id(u32(expr.integer)) last = build_function_value(checker, template, expr.span, frame.expected) _ = pop(&stack) case .Type, .Anonymous_Struct_Type: id := source.add(checker.diagnostics, expr.span, "type is not a runtime value") last = invalid_hir_expr(checker, expr.span, id) _ = pop(&stack) case .Invalid, .Integer: last = invalid_hir_expr(checker, expr.span, expr.diagnostic) _ = pop(&stack) case .Float: last = build_float_expr(checker, expr, frame.expected) _ = pop(&stack) case .Name: last = hir.INVALID_EXPR if !symbol.is_valid(expr.qualifier) { if local, ok := find_build_local(locals, expr.name); ok { last = build_local_expr(checker, local, expr.span) } } else if local, ok := find_build_local(locals, expr.qualifier); ok { base := build_local_expr(checker, local, expr.span) base_type := local.type item, has_item := types.container(base_type, &checker.module.types) field_name := symbol_text(checker, expr.name) if has_item && (item.kind == .Array || item.kind == .Slice) { if field_name == "len" { last = add_hir_expr(checker, hir.Expr{ kind=.Length, span=expr.span, type=types.USIZE, left=base, target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) } else if field_name == "ptr" && (item.kind == .Slice || types.is_pointer(base_type, &checker.module.types)) { last = add_hir_expr(checker, hir.Expr{ kind=.Slice_Ptr, span=expr.span, type=container_pointer_type(&checker.module.types, item), left=base, target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) } else if field_name == "ptr" && item.kind == .Array { id := source.add(checker.diagnostics, expr.span, "arrays do not expose '.ptr'; take their address first") last = invalid_hir_expr(checker, expr.span, id) } } if types.is_pointer(base_type, &checker.module.types) { base_type = types.child_type(base_type, &checker.module.types) } index, field, found := find_struct_field(checker, base_type, expr.name) if last == hir.INVALID_EXPR && found { last = add_hir_expr(checker, hir.Expr{ kind=.Field, span=expr.span, type=field.type, integer=i64(index), left=base, target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) } } if last == hir.INVALID_EXPR && symbol.is_valid(expr.qualifier) && find_import(checker, file, expr.qualifier) == ast.INVALID_IMPORT { if global := find_global(checker, expr.qualifier, pkg, file); global != ast.INVALID_GLOBAL { base := build_global_reference(checker, global, expr.span, global_reads) base_type := checker.global_types[global] item, has_item := types.container(base_type, &checker.module.types) field_name := symbol_text(checker, expr.name) if has_item && (item.kind == .Array || item.kind == .Slice) { if field_name == "len" { last = add_hir_expr(checker, hir.Expr{ kind=.Length, span=expr.span, type=types.USIZE, left=base, target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) } else if field_name == "ptr" && (item.kind == .Slice || types.is_pointer(base_type, &checker.module.types)) { last = add_hir_expr(checker, hir.Expr{ kind=.Slice_Ptr, span=expr.span, type=container_pointer_type(&checker.module.types, item), left=base, target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) } else if field_name == "ptr" && item.kind == .Array { id := source.add(checker.diagnostics, expr.span, "arrays do not expose '.ptr'; take their address first") last = invalid_hir_expr(checker, expr.span, id) } } if types.is_pointer(base_type, &checker.module.types) { base_type = types.child_type(base_type, &checker.module.types) } index, field, found := find_struct_field(checker, base_type, expr.name) if last == hir.INVALID_EXPR && found { last = add_hir_expr(checker, hir.Expr{ kind=.Field, span=expr.span, type=field.type, integer=i64(index), left=base, target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) } } } if last == hir.INVALID_EXPR { if !symbol.is_valid(expr.qualifier) { if value, ok := current_comptime_value(checker, expr.name); ok { if value.kind == .Integer { expected_type := value.type if types.is_concrete_integer(frame.expected) || types.is_float(frame.expected, checker.target) { expected_type = frame.expected } last = build_constant_expr( checker, expr, Constant{kind=.Value, value=value.value}, expected_type, ) } else { id := source.addf( checker.diagnostics, expr.span, "type parameter '%s' is not a runtime value", symbol_text(checker, expr.name), ) last = invalid_hir_expr(checker, expr.span, id) } } } } if last == hir.INVALID_EXPR { if enum_type, enum_ok := enum_type_from_name_expr(checker, expr, pkg, file); enum_ok { last = enum_member_hir(checker, enum_type, expr.name, expr.span) } } if last == hir.INVALID_EXPR { target_pkg, available := expr_package(checker, expr, pkg, file, true) if !available { id := add_package_resolution_diagnostic(checker, expr, file) last = invalid_hir_expr(checker, expr.span, id) } else if global := find_global(checker, expr.name, target_pkg, expr_lookup_file(expr, file)); global != ast.INVALID_GLOBAL { last = build_global_reference(checker, global, expr.span, global_reads) } else { template := find_template(checker, expr.name, target_pkg, expr_lookup_file(expr, file)) if template != ast.INVALID_FUNCTION { last = build_function_value(checker, template, expr.span, frame.expected) } else { id := add_unsupported_diagnostic(checker, expr.span, target_pkg, expr.name) if id == source.INVALID_DIAGNOSTIC { id = add_name_resolution_diagnostic(checker, expr, target_pkg, file) } last = invalid_hir_expr(checker, expr.span, id) } } } _ = pop(&stack) case .Negate: stack[frame_index].stage = 5 append(&stack, Build_Expr_Frame{expr=expr.left, expected=types.INVALID, template=ast.INVALID_FUNCTION}) case .Add, .Sub, .Mul, .Div: stack[frame_index].stage = 1 // Preserve assignment/return context for literal operands, e.g. // assigning `i + 1` back into a `u32` local. left_expected := types.INVALID if types.is_concrete_scalar(frame.expected) && !types.is_bool(frame.expected) { left_expected = frame.expected } append(&stack, Build_Expr_Frame{expr=expr.left, expected=left_expected, template=ast.INVALID_FUNCTION}) case .Call: if expr.left != ast.INVALID_EXPR { stack[frame_index].stage = 6 append(&stack, Build_Expr_Frame{expr=expr.left, expected=types.INVALID, template=ast.INVALID_FUNCTION}) continue } if builtin := type_builtin_call(checker, expr); builtin != .None { last = build_type_builtin(checker, expr, builtin, pkg, file) _ = pop(&stack) continue } if is_ptr_cast_call(checker, expr) { if len(expr.args) != 2 { id := source.addf(checker.diagnostics, expr.span, "ptr_cast expects 2 arguments, got %d", len(expr.args)) last = invalid_hir_expr(checker, expr.span, id) _ = pop(&stack) continue } target, target_ok := resolve_type_argument(checker, expr.args[0], pkg, file) if !target_ok { id := source.add(checker.diagnostics, checker.ast_module.exprs[expr.args[0]].span, "ptr_cast target must be a type") last = invalid_hir_expr(checker, expr.span, id) _ = pop(&stack) continue } if !valid_ptr_cast_child(checker, target) { id := source.addf(checker.diagnostics, checker.ast_module.exprs[expr.args[0]].span, "ptr_cast target must be a sized runtime object type, got %s", type_label(checker, target)) last = invalid_hir_expr(checker, expr.span, id) _ = pop(&stack) continue } stack[frame_index].target_type = target stack[frame_index].stage = 9 append(&stack, Build_Expr_Frame{expr=expr.args[1], expected=types.INVALID, template=ast.INVALID_FUNCTION}) continue } if callee, handled, ok := build_qualified_value_field(checker, expr, locals, global_reads, pkg, file); handled { if !ok { last = callee _ = pop(&stack) continue } stack[frame_index].stage = 6 last = callee continue } target_pkg, available := expr_package(checker, expr, pkg, file, true) if !available { id := add_package_resolution_diagnostic(checker, expr, file) last = invalid_hir_expr(checker, expr.span, id) _ = pop(&stack) continue } template := find_template(checker, expr.name, target_pkg, expr_lookup_file(expr, file)) if template == ast.INVALID_FUNCTION { callee := hir.INVALID_EXPR non_callable := false non_callable_global := false if !symbol.is_valid(expr.qualifier) { if local, ok := find_build_local(locals, expr.name); ok { if _, _, _, callable := types.function_pointer(local.type, &checker.module.types); callable { callee = build_local_expr(checker, local, expr.span) } else { non_callable = true } } } if callee == hir.INVALID_EXPR && !non_callable { if global := find_global(checker, expr.name, target_pkg, expr_lookup_file(expr, file)); global != ast.INVALID_GLOBAL { if _, _, _, callable := types.function_pointer(checker.global_types[global], &checker.module.types); callable { callee = build_global_reference(checker, global, expr.span, global_reads) } else { non_callable = true non_callable_global = true } } } if callee == hir.INVALID_EXPR { distinct_type := types.find_named(&checker.module.types, u32(target_pkg), u32(expr.name), file=u32(expr_lookup_file(expr, file))) distinct_item, distinct_ok := types.node(&checker.module.types, distinct_type) if distinct_ok && distinct_item.kind == .Distinct { if !is_runtime_type(checker, distinct_type) { id := source.addf( checker.diagnostics, expr.span, "distinct type '%s' has no concrete runtime backing type", symbol_text(checker, expr.name), ) last = invalid_hir_expr(checker, expr.span, id) _ = pop(&stack) continue } if len(expr.args) != 1 { id := source.addf( checker.diagnostics, expr.span, "distinct type '%s' expects 1 argument, got %d", symbol_text(checker, expr.name), len(expr.args), ) last = invalid_hir_expr(checker, expr.span, id, distinct_type) _ = pop(&stack) continue } stack[frame_index].target_type = distinct_type stack[frame_index].stage = 8 append(&stack, Build_Expr_Frame{ expr=expr.args[0], expected=distinct_item.child, template=ast.INVALID_FUNCTION, }) continue } id := source.INVALID_DIAGNOSTIC if non_callable { id = add_call_resolution_diagnostic(checker, expr, target_pkg, file) if non_callable_global else source.add(checker.diagnostics, expr.span, "call target is not a function pointer") } else { id = add_unsupported_diagnostic(checker, expr.span, target_pkg, expr.name) if id == source.INVALID_DIAGNOSTIC { id = add_call_resolution_diagnostic(checker, expr, target_pkg, file) } } last = invalid_hir_expr(checker, expr.span, id) _ = pop(&stack) continue } _, function_item, function_type, _ := types.function_pointer(checker.module.exprs[callee].type, &checker.module.types) if !valid_callable_arity(function_item, len(expr.args)) { message := "function pointer expects at least %d arguments, got %d" if function_item.variadic else "function pointer expects %d arguments, got %d" id := source.addf(checker.diagnostics, expr.span, message, function_item.field_count, len(expr.args)) last = invalid_hir_expr(checker, expr.span, id) _ = pop(&stack) continue } stack[frame_index].left = callee stack[frame_index].built_args = make([]hir.Expr_Id, len(expr.args), checker.allocator) stack[frame_index].stage = 7 if len(expr.args) > 0 { arg_expected := callable_arg_expected(function_type, function_item, &checker.module.types, 0) if !is_runtime_type(checker, arg_expected) { arg_expected = types.INVALID } append(&stack, Build_Expr_Frame{expr=expr.args[0], expected=arg_expected, template=ast.INVALID_FUNCTION}) } continue } if len(checker.ast_module.functions[template].unsupported_reason) > 0 { id := source.addf( checker.diagnostics, expr.span, "C declaration '%s' is unavailable: %s", symbol_text(checker, expr.name), checker.ast_module.functions[template].unsupported_reason, ) last = invalid_hir_expr(checker, expr.span, id) _ = pop(&stack) continue } if checker.template_diagnostics[template] != source.INVALID_DIAGNOSTIC { last = invalid_hir_expr(checker, expr.span, checker.template_diagnostics[template]) _ = pop(&stack) continue } function := checker.ast_module.functions[template] if is_type_metatype_syntax(checker, function.result) { id := source.addf(checker.diagnostics, expr.span, "type factory '%s' is only valid in type position", symbol_text(checker, expr.name)) last = invalid_hir_expr(checker, expr.span, id) _ = pop(&stack) continue } mode, prefix := call_argument_mode(function, len(expr.args)) if mode == .Invalid { message := "function '%s' expects at least %d arguments, got %d" if function.variadic else "function '%s' expects %d arguments, got %d" if inferred_prefix, inferable := comptime_prefix_count(function); inferable && !function.c_abi && !function.variadic { id := source.addf( checker.diagnostics, expr.span, "function '%s' expects %d arguments with explicit comptime parameters or %d with inferred comptime parameters, got %d", symbol_text(checker, expr.name), len(function.params), len(function.params)-inferred_prefix, len(expr.args), ) last = invalid_hir_expr(checker, expr.span, id) _ = pop(&stack) continue } id := source.addf( checker.diagnostics, expr.span, message, symbol_text(checker, expr.name), len(function.params), len(expr.args), ) last = invalid_hir_expr(checker, expr.span, id) _ = pop(&stack) continue } stack[frame_index].template = template stack[frame_index].arg_mode = mode stack[frame_index].prefix = prefix stack[frame_index].built_args = make([]hir.Expr_Id, len(expr.args), checker.allocator) stack[frame_index].arg_types = make([]types.Type, len(function.params), checker.allocator) for &arg in stack[frame_index].built_args { arg = hir.INVALID_EXPR } stack[frame_index].arg_index = next_runtime_call_arg(function, mode, prefix, 0, len(expr.args)) stack[frame_index].stage = 3 if stack[frame_index].arg_index < len(expr.args) { param_index := call_param_index(mode, prefix, stack[frame_index].arg_index) arg_expected := call_arg_expected(checker, function, param_index) if !is_runtime_type(checker, arg_expected) { arg_expected = types.INVALID } append(&stack, Build_Expr_Frame{ expr=expr.args[stack[frame_index].arg_index], expected=arg_expected, template=ast.INVALID_FUNCTION, }) } } continue } if frame.stage == 5 { operand := last operand_type := checker.module.exprs[operand].type if !types.is_signed(operand_type, checker.target) && !types.is_float(operand_type, checker.target) { id := source.add(checker.diagnostics, expr.span, "negation requires a signed integer or float") last = invalid_hir_expr(checker, expr.span, id) } else { last = add_hir_expr(checker, hir.Expr{ kind=.Negate, span=expr.span, type=operand_type, left=operand, target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) if types.is_signed(frame.expected, checker.target) || types.is_float(frame.expected, checker.target) { last = coerce_expr(checker, last, frame.expected, expr.span) } } _ = pop(&stack) continue } if frame.stage == 1 { stack[frame_index].left = last stack[frame_index].stage = 2 right_expected := types.INVALID if types.is_many_pointer(checker.module.exprs[last].type, &checker.module.types) { right_expected = types.USIZE } else if eval_constant(checker, expr.right).kind == .Value { // A constant RHS adopts the concrete LHS type before numeric // compatibility is checked. right_expected = checker.module.exprs[last].type } else if types.is_concrete_scalar(frame.expected) && !types.is_bool(frame.expected) { right_expected = frame.expected } append(&stack, Build_Expr_Frame{expr=expr.right, expected=right_expected, template=ast.INVALID_FUNCTION}) continue } if frame.stage == 2 { last = build_binary_arith(checker, expr.kind, frame.left, last, expr.span) _ = pop(&stack) continue } if frame.stage == 3 { if frame.arg_index < len(expr.args) { stack[frame_index].built_args[frame.arg_index] = last param_index := call_param_index(frame.arg_mode, frame.prefix, frame.arg_index) if param_index < len(stack[frame_index].arg_types) { stack[frame_index].arg_types[param_index] = checker.module.exprs[last].type } next := next_runtime_call_arg( checker.ast_module.functions[frame.template], frame.arg_mode, frame.prefix, frame.arg_index+1, len(expr.args), ) stack[frame_index].arg_index = next if next < len(expr.args) { next_param := call_param_index(frame.arg_mode, frame.prefix, next) arg_expected := call_arg_expected(checker, checker.ast_module.functions[frame.template], next_param) if !is_runtime_type(checker, arg_expected) { arg_expected = types.INVALID } append(&stack, Build_Expr_Frame{expr=expr.args[next], expected=arg_expected, template=ast.INVALID_FUNCTION}) continue } } function := checker.ast_module.functions[frame.template] comptime_values: []Comptime_Value comptime_ok := false if frame.arg_mode == .Inferred { comptime_values, comptime_ok = infer_call_comptime_values( checker, function, frame.prefix, expr.args, stack[frame_index].arg_types, frame.expected, pkg, file, diagnose=true, ) } else { comptime_values, comptime_ok = collect_comptime_values(checker, function, expr.args, pkg, file, diagnose=true) } defer delete(comptime_values, checker.allocator) if comptime_ok { previous_comptime := checker.current_comptime_values checker.current_comptime_values = comptime_values for source_index in 0..= len(function.params) || function.params[param_index].comptime_value || !is_numeric_constant_expr(checker, expr.args[source_index]) { continue } expected_arg := call_arg_expected(checker, function, param_index) if !is_runtime_type(checker, expected_arg) { continue } rebuilt := build_nested_expr( checker, expr.args[source_index], locals, global_reads, calls, expected_arg, pkg, file, ) stack[frame_index].built_args[source_index] = rebuilt stack[frame_index].arg_types[param_index] = checker.module.exprs[rebuilt].type } checker.current_comptime_values = previous_comptime } arg_violation := source.INVALID_DIAGNOSTIC if comptime_ok { previous_comptime := checker.current_comptime_values checker.current_comptime_values = comptime_values params := function.params for index in 0..= len(stack[frame_index].arg_types) { break } if params[index].comptime_value { continue } declared := type_from_syntax(checker, params[index].type, function.pkg, function.file) actual := stack[frame_index].arg_types[index] if types.is_constraint(declared) && types.is_valid(actual) && !types.is_valid(types.constraint_target(declared, actual, &checker.module.types)) { arg_violation = source.addf( checker.diagnostics, expr.span, "cannot pass %s to '%s' parameter '%s'", types.name(actual), types.name(declared), symbol_text(checker, params[index].name), ) break } } checker.current_comptime_values = previous_comptime } spec := INVALID_SPEC if comptime_ok { spec = find_spec(checker, frame.template, stack[frame_index].arg_types, comptime_values) } delete(stack[frame_index].arg_types, checker.allocator) stack[frame_index].arg_types = nil if !comptime_ok || arg_violation != source.INVALID_DIAGNOSTIC { delete(stack[frame_index].built_args, checker.allocator) stack[frame_index].built_args = nil diagnostic := arg_violation if diagnostic == source.INVALID_DIAGNOSTIC { diagnostic = source.add(checker.diagnostics, expr.span, "invalid comptime argument") } last = invalid_hir_expr(checker, expr.span, diagnostic) _ = pop(&stack) continue } if spec == INVALID_SPEC { id := source.addf( checker.diagnostics, expr.span, "could not resolve specialization of '%s'", symbol_text(checker, expr.name), ) delete(stack[frame_index].built_args, checker.allocator) stack[frame_index].built_args = nil last = invalid_hir_expr(checker, expr.span, id) _ = pop(&stack) continue } source_args := stack[frame_index].built_args runtime_count := runtime_param_count(function) runtime_arg_count := runtime_count + max(0, len(source_args)-len(function.params)) runtime_args := make([]hir.Expr_Id, runtime_arg_count, checker.allocator) runtime_index := 0 for param, param_index in function.params { if param.comptime_value { continue } source_index := param_index if frame.arg_mode == .Explicit else param_index-frame.prefix arg := source_args[source_index] runtime_args[runtime_index] = coerce_expr( checker, arg, checker.specs[spec].args[runtime_index], checker.module.exprs[arg].span, ) runtime_index += 1 } for source_index in len(function.params).. 0 { arg_expected := callable_arg_expected(function_type, function_item, &checker.module.types, 0) if !is_runtime_type(checker, arg_expected) { arg_expected = types.INVALID } append(&stack, Build_Expr_Frame{expr=expr.args[0], expected=arg_expected, template=ast.INVALID_FUNCTION}) } continue } if frame.stage == 7 { if frame.arg_index < len(expr.args) { stack[frame_index].built_args[frame.arg_index] = last stack[frame_index].arg_index += 1 if frame.arg_index+1 < len(expr.args) { next := frame.arg_index+1 callee_type := checker.module.exprs[frame.left].type _, function_item, function_type, _ := types.function_pointer(callee_type, &checker.module.types) arg_expected := callable_arg_expected(function_type, function_item, &checker.module.types, next) if !is_runtime_type(checker, arg_expected) { arg_expected = types.INVALID } append(&stack, Build_Expr_Frame{expr=expr.args[next], expected=arg_expected, template=ast.INVALID_FUNCTION}) continue } } callee_type := checker.module.exprs[frame.left].type _, function_item, function_type, ok := types.function_pointer(callee_type, &checker.module.types) if !ok { id := source.add(checker.diagnostics, expr.span, "call target is not a function pointer") delete(stack[frame_index].built_args, checker.allocator) stack[frame_index].built_args = nil last = invalid_hir_expr(checker, expr.span, id) _ = pop(&stack) continue } fixed_count := int(function_item.field_count) for index in 0.. string { spec := checker.specs[id] function := checker.ast_module.functions[spec.template] if function.pkg == 0 && function.name == checker.main_symbol { return fmt.aprintf("main", allocator = checker.allocator) } if function.generated { return fmt.aprintf("bro__p%d__anon%d", function.pkg, spec.template, allocator = checker.allocator) } if !function.has_body && function.c_abi { if len(function.link_name) > 0 { return strings.clone(function.link_name, checker.allocator) } return fmt.aprintf("%s", symbol_text(checker, function.name), allocator = checker.allocator) } builder := strings.builder_make(checker.allocator) defer strings.builder_destroy(&builder) strings.write_string(&builder, "bro_c__" if function.c_abi else "bro__") fmt.sbprintf(&builder, "p%d__", function.pkg) if function.file_hidden { fmt.sbprintf(&builder, "f%d__", function.file) } strings.write_string(&builder, symbol_text(checker, function.name)) for arg in spec.args { strings.write_string(&builder, "__") if arg >= types.DYNAMIC_START { fmt.sbprintf(&builder, "t%d", arg) } else { strings.write_string(&builder, types.name(arg)) } } for value in spec.comptime_values { if value.kind == .Type { strings.write_string(&builder, "__ct") if value.type >= types.DYNAMIC_START { fmt.sbprintf(&builder, "t%d", value.type) } else { strings.write_string(&builder, types.name(value.type)) } } else { strings.write_string(&builder, "__cv") if value.value < 0 { strings.write_string(&builder, "n") fmt.sbprintf(&builder, "%d", -value.value) } else { fmt.sbprintf(&builder, "%d", value.value) } } } return fmt.aprintf("%s", strings.to_string(builder), allocator = checker.allocator) } // Replay the deferred statements in frames `[lo, len(defers))` into `body`, // innermost-most-recent first (LIFO across frames); each frame's own statements // keep their forward order. Used at every scope-exit path in `build_block`. flush_defers :: proc(ctx: ^Build_Ctx, body: ^[dynamic]hir.Stmt_Id, lo: int) { for i := len(ctx.defers^) - 1; i >= lo; i -= 1 { for stmt_id in ctx.defers^[i] { append(body, stmt_id) } } } build_block :: proc( ctx: ^Build_Ctx, statements: []ast.Stmt_Id, duplicate_scope_start := -1, close := true, ) -> []hir.Stmt_Id { checker := ctx.checker store := &checker.module.types body: [dynamic]hir.Stmt_Id body.allocator = checker.allocator scope_start := len(ctx.locals^) defer_start := len(ctx.defers^) duplicate_start := scope_start if duplicate_scope_start < 0 else duplicate_scope_start for statement_id in statements { statement := checker.ast_module.statements[statement_id] switch statement.kind { case .Declaration: // A value block (`x :: { ... yield v }` / `x T = { ... }`): the parser // leaves `expr` invalid and stashes the block in `body`. Build it, then // declare the local from the yielded value (its type for an untyped `::`). if statement.expr == ast.INVALID_EXPR { expected := types.INVALID typed := is_runtime_type(checker, type_from_syntax(checker, statement.type, ctx.pkg, ctx.file)) if typed { expected = type_from_syntax(checker, statement.type, ctx.pkg, ctx.file) } value, value_type := build_value_source(ctx, &body, statement.body, expected, statement.span, statement.label, statement.value_control_flow) if _, found := find_build_local(ctx.locals^[duplicate_start:], statement.name); found { id := source.addf( checker.diagnostics, statement.span, "duplicate local '%s'", symbol_text(checker, statement.name), ) append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR, local = hir.INVALID_LOCAL, diagnostic = id, }) ctx.problematic^ = true continue } if id := add_shadow_diagnostic( checker, statement.span, statement.name, "local", ctx.pkg, ctx.file, ctx.locals^[:], ctx.loop_labels^[:], ctx.yield_targets^[:], ); id != source.INVALID_DIAGNOSTIC { append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR, local = hir.INVALID_LOCAL, diagnostic = id, }) ctx.problematic^ = true continue } local_id := append_build_local(ctx, statement.name, value_type, !statement.immutable, statement.span) append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Declaration, span = statement.span, local = local_id, expr = value, diagnostic = source.INVALID_DIAGNOSTIC, }) continue } declared := resolve_inferred_array(checker, type_from_syntax(checker, statement.type, ctx.pkg, ctx.file), statement.expr) // Adopt the type inference resolved for this local when the declaration has no // concrete annotation and inference carried useful numeric context: constraints, // `undefined`, open numeric constants, or arithmetic expressions. open_const_decl := !is_runtime_type(checker, declared) && !is_undefined_expr(checker, statement.expr) if open_const_decl { constant := eval_integer_constant_in_context(checker, statement.expr, ctx.pkg, ctx.file) open_const_decl = constant.kind == .Value && fits_i64(constant.value) || is_float_constant_expr(checker, statement.expr) } numeric_arithmetic_decl := !is_runtime_type(checker, declared) && is_numeric_arithmetic_expr(checker, statement.expr) if statement_id != ast.INVALID_STMT && int(statement_id) < len(ctx.local_types) && is_runtime_type(checker, ctx.local_types[statement_id]) && (types.is_constraint(declared) || is_undefined_expr(checker, statement.expr) || open_const_decl || numeric_arithmetic_decl || has_inferred_array_count(checker, declared)) { declared = ctx.local_types[statement_id] } // A still-unresolved constraint means the initializer's numeric // family did not satisfy `int`/`float` (`undefined` reports its own). if types.is_constraint(declared) && !is_undefined_expr(checker, statement.expr) { id := source.addf( checker.diagnostics, statement.span, "could not resolve the '%s' constraint for local '%s'", types.name(declared), symbol_text(checker, statement.name), ) append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR, local = hir.INVALID_LOCAL, diagnostic = id, }) ctx.problematic^ = true continue } expected := types.INVALID value := hir.INVALID_EXPR value_type := types.INVALID if is_undefined_expr(checker, statement.expr) { if statement.immutable { id := source.add( checker.diagnostics, statement.span, "'undefined' requires a mutable local declaration", ) append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR, local = hir.INVALID_LOCAL, diagnostic = id, }) ctx.problematic^ = true continue } if !is_runtime_type(checker, declared) || has_inferred_array_count(checker, declared) { id := source.addf( checker.diagnostics, statement.span, "could not infer a concrete type for local '%s'", symbol_text(checker, statement.name), ) append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR, local = hir.INVALID_LOCAL, diagnostic = id, }) ctx.problematic^ = true continue } value_type = declared } else { if is_runtime_type(checker, declared) { expected = declared } value = build_expr( checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, expected, ctx.pkg, ctx.file, ) value_type = checker.module.exprs[value].type if is_runtime_type(checker, declared) { value = coerce_expr(checker, value, declared, statement.span) value_type = checker.module.exprs[value].type } else if types.is_void(declared) { id := source.add(checker.diagnostics, statement.span, "locals cannot have type void") value = invalid_hir_expr(checker, statement.span, id) value_type = types.INVALID } } if _, found := find_build_local(ctx.locals^[duplicate_start:], statement.name); found { id := source.addf( checker.diagnostics, statement.span, "duplicate local '%s'", symbol_text(checker, statement.name), ) append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR, local = hir.INVALID_LOCAL, diagnostic = id, }) ctx.problematic^ = true continue } if id := add_shadow_diagnostic( checker, statement.span, statement.name, "local", ctx.pkg, ctx.file, ctx.locals^[:], ctx.loop_labels^[:], ctx.yield_targets^[:], ); id != source.INVALID_DIAGNOSTIC { append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR, local = hir.INVALID_LOCAL, diagnostic = id, }) ctx.problematic^ = true continue } local_id := append_build_local(ctx, statement.name, value_type, !statement.immutable, statement.span) append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Declaration, span = statement.span, local = local_id, expr = value, diagnostic = source.INVALID_DIAGNOSTIC, }) if value != hir.INVALID_EXPR { ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid } case .Assignment: if statement.target != ast.INVALID_EXPR { target_expr := build_expr( checker, statement.target, ctx.locals^[:], ctx.global_reads, ctx.calls, types.INVALID, ctx.pkg, ctx.file, ) target_type := checker.module.exprs[target_expr].type if !hir_location_writable(checker, target_expr, ctx.locals^[:]) { id := source.add(checker.diagnostics, statement.span, "assignment target is not writable") append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind=.Trap, span=statement.span, local=hir.INVALID_LOCAL, target=hir.INVALID_EXPR, expr=hir.INVALID_EXPR, diagnostic=id, }) ctx.problematic^ = true continue } value: hir.Expr_Id assignment_op := hir.Assignment_Op.Set if statement.assignment_op != .Set { rhs_expected := target_type if types.is_many_pointer(target_type, &checker.module.types) { rhs_expected = types.USIZE if statement.assignment_op == .Add else types.INVALID } value = build_expr( checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, rhs_expected, ctx.pkg, ctx.file, ) if types.is_many_pointer(target_type, &checker.module.types) { assignment_op = .Pointer_Add if statement.assignment_op != .Add { id := source.add( checker.diagnostics, statement.span, "many-item pointers only support '+=' compound assignment", ) value = invalid_hir_expr(checker, statement.span, id, types.USIZE) } else { value = coerce_expr(checker, value, types.USIZE, statement.span) } } else { #partial switch statement.assignment_op { case .Add: assignment_op = .Add case .Sub: assignment_op = .Sub case .Mul: assignment_op = .Mul case .Div: assignment_op = .Div } rhs_type := checker.module.exprs[value].type result_type := types.widest(target_type, rhs_type) if !types.is_concrete_scalar(result_type) || types.is_bool(result_type) { id := source.add( checker.diagnostics, statement.span, "arithmetic requires compatible numeric operands", ) value = invalid_hir_expr(checker, statement.span, id, target_type) } else { // Compound assignment stores back into the original // target type, so only an equal or widening RHS // conversion is permitted. value = coerce_expr(checker, value, target_type, statement.span) } } } else if statement.expr == ast.INVALID_EXPR { // `target = { ... yield v }`: build the value block against the // target's type (build_value_block coerces internally). value, _ = build_value_source(ctx, &body, statement.body, target_type, statement.span, statement.label, statement.value_control_flow) } else { value = build_expr( checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, target_type, ctx.pkg, ctx.file, ) value = coerce_expr(checker, value, target_type, statement.span) } append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind=.Assignment, span=statement.span, local=hir.INVALID_LOCAL, assignment_op=assignment_op, target=target_expr, expr=value, diagnostic=source.INVALID_DIAGNOSTIC, }) ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid continue } if statement.name == checker.sink_symbol { value: hir.Expr_Id if statement.expr == ast.INVALID_EXPR { value, _ = build_value_source(ctx, &body, statement.body, types.INVALID, statement.span, statement.label, statement.value_control_flow) } else { value = build_expr(checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, types.INVALID, ctx.pkg, ctx.file) } if types.is_void(checker.module.exprs[value].type) { id := source.add(checker.diagnostics, statement.span, "cannot assign a void expression to '_'") append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR, local = hir.INVALID_LOCAL, diagnostic = id, }) ctx.problematic^ = true } else { append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Sink, span = statement.span, expr = value, local = hir.INVALID_LOCAL, diagnostic = source.INVALID_DIAGNOSTIC, }) } continue } if _, is_comptime := current_comptime_value(checker, statement.name); is_comptime { id := source.addf( checker.diagnostics, statement.span, "cannot assign comptime parameter '%s'", symbol_text(checker, statement.name), ) append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR, local = hir.INVALID_LOCAL, diagnostic = id, }) ctx.problematic^ = true continue } local, found := find_build_local(ctx.locals^[:], statement.name) if !found { global := find_global(checker, statement.name, ctx.pkg, ctx.file) if global == ast.INVALID_GLOBAL { id := source.addf(checker.diagnostics, statement.span, "cannot assign unresolved local '%s'", symbol_text(checker, statement.name)) append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR, local = hir.INVALID_LOCAL, diagnostic = id, }) ctx.problematic^ = true continue } target_expr := build_global_reference(checker, global, statement.span, ctx.global_reads) target_type := checker.module.exprs[target_expr].type if !hir_location_writable(checker, target_expr, ctx.locals^[:]) { id := source.add(checker.diagnostics, statement.span, "assignment target is not writable") append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind=.Trap, span=statement.span, local=hir.INVALID_LOCAL, target=hir.INVALID_EXPR, expr=hir.INVALID_EXPR, diagnostic=id, }) ctx.problematic^ = true continue } value: hir.Expr_Id if statement.expr == ast.INVALID_EXPR { value, _ = build_value_source(ctx, &body, statement.body, target_type, statement.span, statement.label, statement.value_control_flow) } else { value = build_expr( checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, target_type, ctx.pkg, ctx.file, ) value = coerce_expr(checker, value, target_type, statement.span) } append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind=.Assignment, span=statement.span, local=hir.INVALID_LOCAL, assignment_op=.Set, target=target_expr, expr=value, diagnostic=source.INVALID_DIAGNOSTIC, }) ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid continue } if !local.mutable { id := source.addf(checker.diagnostics, statement.span, "cannot assign immutable local '%s'", symbol_text(checker, statement.name)) append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR, local = hir.INVALID_LOCAL, diagnostic = id, }) ctx.problematic^ = true continue } value: hir.Expr_Id if statement.expr == ast.INVALID_EXPR { value, _ = build_value_source(ctx, &body, statement.body, local.type, statement.span, statement.label, statement.value_control_flow) } else { value = build_expr( checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, local.type, ctx.pkg, ctx.file, ) value = coerce_expr(checker, value, local.type, statement.span) } append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Assignment, span = statement.span, expr = value, local = local.id, target = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC, }) ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid case .Return: if ctx.defer_depth > 0 { id := source.add(checker.diagnostics, statement.span, "cannot 'return' inside a 'defer'") append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR, local = hir.INVALID_LOCAL, diagnostic = id, }) ctx.problematic^ = true continue } if statement.expr == ast.INVALID_EXPR && !statement.value_control_flow { if types.kind(ctx.result, store) == .Fallible && types.is_void(types.fallible_success(ctx.result, store)) { flush_defers(ctx, &body, 0) value := fallible_aggregate(checker, statement.span, ctx.result, hir.INVALID_EXPR, false) append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Return, span = statement.span, expr = value, local = hir.INVALID_LOCAL, diagnostic = source.INVALID_DIAGNOSTIC, }) } else if !types.is_void(ctx.result) { id := source.add(checker.diagnostics, statement.span, "'return _' is only valid in a void function") append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR, local = hir.INVALID_LOCAL, diagnostic = id, }) ctx.problematic^ = true } else { flush_defers(ctx, &body, 0) append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Return, span = statement.span, expr = hir.INVALID_EXPR, local = hir.INVALID_LOCAL, diagnostic = source.INVALID_DIAGNOSTIC, }) } continue } if types.is_void(ctx.result) { id := source.add(checker.diagnostics, statement.span, "void function cannot return a value") append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR, local = hir.INVALID_LOCAL, diagnostic = id, }) ctx.problematic^ = true continue } value := hir.INVALID_EXPR if statement.value_control_flow { if types.kind(ctx.result, store) == .Fallible { success := types.fallible_success(ctx.result, store) value, _ = build_value_source(ctx, &body, statement.body, success, statement.span, symbol.INVALID, statement.value_control_flow) value = coerce_expr(checker, value, success, statement.span) value = fallible_aggregate(checker, statement.span, ctx.result, value, false) } else { value, _ = build_value_source(ctx, &body, statement.body, ctx.result, statement.span, symbol.INVALID, statement.value_control_flow) value = coerce_expr(checker, value, ctx.result, statement.span) } } else if types.kind(ctx.result, store) == .Fallible { success := types.fallible_success(ctx.result, store) error_type := types.fallible_error(ctx.result, store) error_path := false expr_ast := checker.ast_module.exprs[statement.expr] if expr_ast.kind == .Enum_Literal { success_has := types.sum_has_name(store, success, u32(expr_ast.name)) error_has := types.sum_has_name(store, error_type, u32(expr_ast.name)) if error_has && !success_has { error_path = true } else if error_has && success_has { id := source.add(checker.diagnostics, expr_ast.span, "ambiguous fallible return member") value = invalid_hir_expr(checker, expr_ast.span, id, ctx.result) } } else if expr_ast.kind == .Struct_Literal { target_pkg, available := expr_package(checker, expr_ast, ctx.pkg, ctx.file, true) named := types.find_named(store, u32(target_pkg), u32(expr_ast.name), file=u32(expr_lookup_file(expr_ast, ctx.file))) if available else types.INVALID named = types.resolve_alias(named, store) error_path = can_implicitly_convert_type(checker, named, error_type) } else { probe := build_expr( checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, types.INVALID, ctx.pkg, ctx.file, ) probe_type := checker.module.exprs[probe].type if can_implicitly_convert_type(checker, probe_type, error_type) && !can_implicitly_convert_type(checker, probe_type, success) { error_path = true value = probe } else if can_implicitly_convert_type(checker, probe_type, success) { value = probe } } if value == hir.INVALID_EXPR { expected := error_type if error_path else success value = build_expr( checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, expected, ctx.pkg, ctx.file, ) } expected := error_type if error_path else success value = coerce_expr(checker, value, expected, statement.span) value = fallible_aggregate(checker, statement.span, ctx.result, value, error_path) } else { value = build_expr( checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, ctx.result, ctx.pkg, ctx.file, ) value = coerce_expr(checker, value, ctx.result, statement.span) } ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid // Run deferred statements before returning, but capture the return value // first (spill it to a temp) so a defer that mutates the returned local // can't change what is returned — Zig evaluates the return value, then // runs defers. if len(ctx.defers^) > 0 { if checker.module.exprs[value].kind != .Invalid { tmp := append_tracked_local( ctx.hir_locals, ctx.local_spans, ctx.local_used, ctx.local_warnable, hir.Local{name = checker.sink_symbol, type = ctx.result, mutable = false}, source.Span{}, ) append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Declaration, span = statement.span, local = tmp, expr = value, diagnostic = source.INVALID_DIAGNOSTIC, }) value = hir.expr_id(len(checker.module.exprs)) append(&checker.module.exprs, hir.Expr{ kind = .Local, span = statement.span, type = ctx.result, target = hir.local_ref(tmp), }) } flush_defers(ctx, &body, 0) } append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Return, span = statement.span, expr = value, local = hir.INVALID_LOCAL, diagnostic = source.INVALID_DIAGNOSTIC, }) case .Expression: value := build_expr(checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, types.INVALID, ctx.pkg, ctx.file) if !types.is_void(checker.module.exprs[value].type) { id := source.add(checker.diagnostics, statement.span, "non-void expression result must be consumed or assigned to '_'") append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR, local = hir.INVALID_LOCAL, diagnostic = id, }) ctx.problematic^ = true } else { append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Expression, span = statement.span, expr = value, local = hir.INVALID_LOCAL, diagnostic = source.INVALID_DIAGNOSTIC, }) } case .If: if len(statement.captures) > 0 { ast_operands: [dynamic]ast.Expr_Id ast_operands.allocator = checker.allocator flatten_conditional_unwrap_operands(checker.ast_module, statement.expr, &ast_operands) valid_unwrap := true diagnostic := source.INVALID_DIAGNOSTIC if len(ast_operands) != len(statement.captures) { diagnostic = source.addf( checker.diagnostics, statement.span, "'if' unwrap has %d operands but %d captures", len(ast_operands), len(statement.captures), ) valid_unwrap = false } values := make([]hir.Expr_Id, len(ast_operands), checker.allocator) child_types := make([]types.Type, len(ast_operands), checker.allocator) for operand, index in ast_operands { child_types[index] = types.INVALID value := build_expr( checker, operand, ctx.locals^[:], ctx.global_reads, ctx.calls, types.INVALID, ctx.pkg, ctx.file, ) values[index] = value value_type := checker.module.exprs[value].type if checker.module.exprs[value].kind == .Invalid { valid_unwrap = false if diagnostic == source.INVALID_DIAGNOSTIC { diagnostic = checker.module.exprs[value].diagnostic } } else if !types.is_optional(value_type, &checker.module.types) { diagnostic = source.addf( checker.diagnostics, checker.ast_module.exprs[operand].span, "'if' unwrap requires an optional value (operand %d)", index + 1, ) valid_unwrap = false } else { child_types[index] = types.child_type(value_type, &checker.module.types) } } capture_start := len(ctx.locals^) unwraps: [dynamic]hir.Conditional_Unwrap unwraps.allocator = checker.allocator for capture, index in statement.captures { child := child_types[index] if index < len(child_types) else types.INVALID local := hir.INVALID_LOCAL if capture != checker.sink_symbol { if _, duplicate := find_build_local(ctx.locals^[capture_start:], capture); duplicate { diagnostic = source.add( checker.diagnostics, statement.span, "'if' unwrap captures must have distinct names", ) valid_unwrap = false } else if id := add_shadow_diagnostic( checker, statement.span, capture, "capture", ctx.pkg, ctx.file, ctx.locals^[:capture_start], ctx.loop_labels^[:], ctx.yield_targets^[:], ); id != source.INVALID_DIAGNOSTIC { diagnostic = id valid_unwrap = false } local = append_build_local(ctx, capture, child, false, statement.span) } if index < len(values) { append(&unwraps, hir.Conditional_Unwrap{expr=values[index], local=local}) } } guard := hir.INVALID_EXPR if statement.guard != ast.INVALID_EXPR { guard = build_expr( checker, statement.guard, ctx.locals^[:], ctx.global_reads, ctx.calls, types.BOOL, ctx.pkg, ctx.file, ) if checker.module.exprs[guard].kind == .Invalid { valid_unwrap = false if diagnostic == source.INVALID_DIAGNOSTIC { diagnostic = checker.module.exprs[guard].diagnostic } } else if !types.is_bool(checker.module.exprs[guard].type) { diagnostic = source.add( checker.diagnostics, checker.ast_module.exprs[statement.guard].span, "'if' unwrap guard must be a bool", ) valid_unwrap = false } } then_body := build_block(ctx, statement.body, capture_start) resize(ctx.locals, capture_start) else_body: []hir.Stmt_Id = nil if statement.else_body != nil { else_body = build_block(ctx, statement.else_body) } append(&body, hir.stmt_id(len(checker.module.statements))) if valid_unwrap { append(&checker.module.statements, hir.Stmt{ kind=.If, span=statement.span, expr=hir.INVALID_EXPR, unwraps=unwraps[:], guard=guard, then_body=then_body, else_body=else_body, local=hir.INVALID_LOCAL, target=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) } else { delete(unwraps) delete(then_body, checker.allocator) delete(else_body, checker.allocator) append(&checker.module.statements, hir.Stmt{ kind=.Trap, span=statement.span, expr=hir.INVALID_EXPR, guard=hir.INVALID_EXPR, local=hir.INVALID_LOCAL, target=hir.INVALID_EXPR, diagnostic=diagnostic, }) ctx.problematic^ = true } delete(values, checker.allocator) delete(child_types, checker.allocator) delete(ast_operands) continue } condition := build_expr(checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, types.BOOL, ctx.pkg, ctx.file) if checker.module.exprs[condition].kind != .Invalid && !types.is_bool(checker.module.exprs[condition].type) { id := source.add(checker.diagnostics, statement.span, "'if' condition must be a bool") condition = invalid_hir_expr(checker, statement.span, id, types.BOOL) ctx.problematic^ = true } then_body := build_block(ctx, statement.body) else_body: []hir.Stmt_Id = nil if statement.else_body != nil { else_body = build_block(ctx, statement.else_body) } append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .If, span = statement.span, expr = condition, guard = hir.INVALID_EXPR, then_body = then_body, else_body = else_body, local = hir.INVALID_LOCAL, target = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC, }) ctx.problematic^ = ctx.problematic^ || checker.module.exprs[condition].kind == .Invalid case .While: condition := build_expr( checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, types.BOOL, ctx.pkg, ctx.file, ) if checker.module.exprs[condition].kind != .Invalid && !types.is_bool(checker.module.exprs[condition].type) { id := source.add(checker.diagnostics, statement.span, "'while' condition must be a bool") condition = invalid_hir_expr(checker, statement.span, id, types.BOOL) ctx.problematic^ = true } if id := add_label_shadow_diagnostic(ctx, statement.span, statement.label); id != source.INVALID_DIAGNOSTIC { append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR, local = hir.INVALID_LOCAL, diagnostic = id, }) ctx.problematic^ = true } append(ctx.loop_defer_starts, len(ctx.defers^)) append(ctx.loop_labels, statement.label) append(ctx.loop_is_loop, true) loop_body := build_block(ctx, statement.body) pop(ctx.loop_is_loop) pop(ctx.loop_labels) pop(ctx.loop_defer_starts) update := hir.INVALID_STMT if statement.update != ast.INVALID_STMT { update_ast := [1]ast.Stmt_Id{statement.update} update_body := build_block(ctx, update_ast[:]) if len(update_body) > 0 { update = update_body[0] } delete(update_body, checker.allocator) } append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind=.While, span=statement.span, label=statement.label, expr=condition, then_body=loop_body, update=update, local=hir.INVALID_LOCAL, target=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) ctx.problematic^ = ctx.problematic^ || checker.module.exprs[condition].kind == .Invalid case .For: iterable := build_expr( checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, types.INVALID, ctx.pkg, ctx.file, ) iterable_type := checker.module.exprs[iterable].type capture_type := types.I64 iterator_type := types.INVALID valid_loop := checker.module.exprs[iterable].kind != .Invalid diagnostic := source.INVALID_DIAGNOSTIC is_range := types.is_range(iterable_type, &checker.module.types) if is_range { capture_type = types.child_type(iterable_type, &checker.module.types) if statement.pointer_capture { diagnostic = source.add(checker.diagnostics, statement.span, "range loops do not support pointer captures") valid_loop = false } if symbol.is_valid(statement.index_name) { diagnostic = source.add(checker.diagnostics, statement.span, "range loops do not support index captures") valid_loop = false } } else { item, ok := sequence_item(iterable_type, &checker.module.types) if !ok { diagnostic = source.add( checker.diagnostics, statement.span, "for-loop iterable must be a range, array, slice, or pointer-to-array", ) valid_loop = false } else { iterator_type = types.pointer( &checker.module.types, item.child, item.mutable, true, item.has_sentinel, item.sentinel, ) capture_type = item.child if statement.pointer_capture { _, _, array_pointer_ok := types.array_pointer(iterable_type, &checker.module.types) if !types.is_slice(iterable_type, &checker.module.types) && !array_pointer_ok { diagnostic = source.add( checker.diagnostics, statement.span, "pointer capture over an array requires a pointer-to-array such as '&items'", ) valid_loop = false } capture_type = types.pointer(&checker.module.types, item.child, item.mutable, false) } } } capture_start := len(ctx.locals^) if statement.name != checker.sink_symbol { if id := add_shadow_diagnostic( checker, statement.span, statement.name, "capture", ctx.pkg, ctx.file, ctx.locals^[:], ctx.loop_labels^[:], ctx.yield_targets^[:], ); id != source.INVALID_DIAGNOSTIC { diagnostic = id valid_loop = false } } item_local := append_build_local(ctx, statement.name, capture_type, false, statement.span) index_local := hir.INVALID_LOCAL if symbol.is_valid(statement.index_name) { if statement.index_name == statement.name { diagnostic = source.add(checker.diagnostics, statement.span, "for-loop captures must have distinct names") valid_loop = false } else if id := add_shadow_diagnostic( checker, statement.span, statement.index_name, "capture", ctx.pkg, ctx.file, ctx.locals^[:capture_start], ctx.loop_labels^[:], ctx.yield_targets^[:], ); id != source.INVALID_DIAGNOSTIC { diagnostic = id valid_loop = false } else { index_local = append_build_local(ctx, statement.index_name, types.USIZE, false, statement.span) } } if id := add_label_shadow_diagnostic(ctx, statement.span, statement.label); id != source.INVALID_DIAGNOSTIC { append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR, local = hir.INVALID_LOCAL, diagnostic = id, }) ctx.problematic^ = true } append(ctx.loop_defer_starts, len(ctx.defers^)) append(ctx.loop_labels, statement.label) append(ctx.loop_is_loop, true) loop_body := build_block(ctx, statement.body, capture_start) pop(ctx.loop_is_loop) pop(ctx.loop_labels) pop(ctx.loop_defer_starts) resize(ctx.locals, capture_start) append(&body, hir.stmt_id(len(checker.module.statements))) if valid_loop { append(&checker.module.statements, hir.Stmt{ kind=.For, span=statement.span, label=statement.label, local=item_local, index_local=index_local, expr=iterable, iterator_type=iterator_type, pointer_capture=statement.pointer_capture, then_body=loop_body, update=hir.INVALID_STMT, target=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) } else { delete(loop_body, checker.allocator) append(&checker.module.statements, hir.Stmt{ kind=.Trap, span=statement.span, local=hir.INVALID_LOCAL, index_local=hir.INVALID_LOCAL, expr=hir.INVALID_EXPR, target=hir.INVALID_EXPR, diagnostic=diagnostic, }) ctx.problematic^ = true } case .Break, .Continue: // `break :L` / `continue :L` targets the innermost enclosing loop labeled `L`; // an unlabeled one targets the innermost loop. Inside a `defer`, `loop_floor` // hides the loops opened outside the defer. target_index := -1 if symbol.is_valid(statement.label) { // `break :L` targets a labeled loop or block; `continue :L` only a loop. for i := len(ctx.loop_labels^) - 1; i >= ctx.loop_floor; i -= 1 { if ctx.loop_labels^[i] == statement.label && (ctx.loop_is_loop^[i] || statement.kind == .Break) { target_index = i break } } } else { // Unlabeled `break`/`continue` targets the innermost loop, skipping blocks. for i := len(ctx.loop_defer_starts^) - 1; i >= ctx.loop_floor; i -= 1 { if ctx.loop_is_loop^[i] { target_index = i break } } } if target_index < 0 { keyword := "break" if statement.kind == .Break else "continue" id: source.Diagnostic_Id if symbol.is_valid(statement.label) { id = source.addf(checker.diagnostics, statement.span, "no enclosing loop is labeled '%s'", symbol_text(checker, statement.label)) } else { id = source.addf(checker.diagnostics, statement.span, "'%s' outside of a loop", keyword) } append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR, local = hir.INVALID_LOCAL, diagnostic = id, }) ctx.problematic^ = true continue } // Exit the loop body and any blocks between here and the target loop: run their // deferred statements down to and including the target loop body. flush_defers(ctx, &body, ctx.loop_defer_starts^[target_index]) append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Break if statement.kind == .Break else .Continue, span = statement.span, label = statement.label, expr = hir.INVALID_EXPR, local = hir.INVALID_LOCAL, diagnostic = source.INVALID_DIAGNOSTIC, }) case .Block: if symbol.is_valid(statement.label) { // A labeled block statement (`blk: { … break :blk … }`): a break target // with an exit-label boundary, built as a HIR `.Block`. Not a loop, so // unlabeled `break`/`continue` and `continue :blk` skip it. if id := add_label_shadow_diagnostic(ctx, statement.span, statement.label); id != source.INVALID_DIAGNOSTIC { append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR, local = hir.INVALID_LOCAL, diagnostic = id, }) ctx.problematic^ = true } append(ctx.loop_defer_starts, len(ctx.defers^)) append(ctx.loop_labels, statement.label) append(ctx.loop_is_loop, false) built := build_block(ctx, statement.body) pop(ctx.loop_is_loop) pop(ctx.loop_labels) pop(ctx.loop_defer_starts) append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Block, span = statement.span, label = statement.label, then_body = built, local = hir.INVALID_LOCAL, target = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC, }) continue } // A bare `{ ... }` scope: build it (its own locals/defers are scoped by // the recursive call) and splice its statements in. block := build_block(ctx, statement.body) for stmt in block { append(&body, stmt) } delete(block, checker.allocator) case .Yield: // A labeled `yield :blk x` exits the value-loop labeled `blk`: assign the // result slot, then `break` (which flushes defers down to the loop body and // branches to its exit). HIR holds no `.Yield` — it becomes Assignment + Break. if symbol.is_valid(statement.label) { target_index := -1 for i := len(ctx.yield_targets^) - 1; i >= 0; i -= 1 { if ctx.yield_targets^[i].label == statement.label { target_index = i break } } if target_index < 0 { id := source.addf(checker.diagnostics, statement.span, "no enclosing value loop or block is labeled '%s'", symbol_text(checker, statement.label)) append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR, local = hir.INVALID_LOCAL, diagnostic = id, }) ctx.problematic^ = true continue } target := &ctx.yield_targets^[target_index] yielded := hir.INVALID_EXPR if statement.value_control_flow { yielded, _ = build_value_source(ctx, &body, statement.body, target.slot_type, statement.span, symbol.INVALID, statement.value_control_flow) } else { yielded = build_expr(checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, target.slot_type, ctx.pkg, ctx.file) } yielded = resolve_loop_slot(ctx, target, yielded, checker.module.exprs[yielded].type if yielded != hir.INVALID_EXPR else types.INVALID, statement.span) if target.slot == hir.INVALID_LOCAL || yielded == hir.INVALID_EXPR { id := source.add(checker.diagnostics, statement.span, "could not determine the value loop's yield type; annotate the binding or yield a concrete value first") append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR, local = hir.INVALID_LOCAL, diagnostic = id, }) ctx.problematic^ = true continue } // slot = value (the slot is un-nameable, so no defer can mutate it; no spill). emit_slot_assign(checker, &body, target.slot, yielded, statement.span) // Exit the target: flush defers down to its body, then a labeled break. flush_defers(ctx, &body, target.defer_floor) append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Break, span = statement.span, label = target.label, expr = hir.INVALID_EXPR, local = hir.INVALID_LOCAL, diagnostic = source.INVALID_DIAGNOSTIC, }) continue } // An unlabeled yield reaching here is misplaced: a legitimate trailing yield // is peeled by the value builders (value block / if branch / loop fall-through). id := source.add( checker.diagnostics, statement.span, "'yield' is only valid as the final statement of a value block, or as 'yield :label' inside a labeled value loop or block", ) append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR, local = hir.INVALID_LOCAL, diagnostic = id, }) ctx.problematic^ = true case .Defer: deferred := checker.ast_module.statements[statement.update] if deferred.kind == .Return || deferred.kind == .Break || deferred.kind == .Continue || deferred.kind == .Defer { keyword := "return" if deferred.kind == .Break { keyword = "break" } if deferred.kind == .Continue { keyword = "continue" } if deferred.kind == .Defer { keyword = "defer" } id := source.addf(checker.diagnostics, statement.span, "cannot defer a '%s' statement", keyword) append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR, local = hir.INVALID_LOCAL, diagnostic = id, }) ctx.problematic^ = true continue } // Build the deferred statement once, guarded so a `return` inside it is // rejected and `break`/`continue` only target loops opened within the // defer; its hir is replayed at each scope exit, not emitted here. saved_floor := ctx.loop_floor ctx.defer_depth += 1 ctx.loop_floor = len(ctx.loop_defer_starts^) entry := build_block(ctx, []ast.Stmt_Id{statement.update}) ctx.loop_floor = saved_floor ctx.defer_depth -= 1 append(ctx.defers, entry) case .Match: build_match(ctx, &body, statement) case .Match_Arm: // Arms are only reachable through their enclosing `.Match`; one on its own // is a parser bug. append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR, local = hir.INVALID_LOCAL, diagnostic = source.add(checker.diagnostics, statement.span, "unexpected match arm outside 'match'"), }) ctx.problematic^ = true case .Invalid: append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR, local = hir.INVALID_LOCAL, diagnostic = statement.diagnostic, }) ctx.problematic^ = true } } // Normal fall-through exit: run this block's own deferred statements, unless // every path already exited early (return/break/continue) — that would only // emit unreachable duplicates. A value block (`close=false`) skips this so its // caller can capture the yielded value before flushing the block's defers. if close { if !all_paths_exit(&checker.module, body[:]) { flush_defers(ctx, &body, defer_start) } // Free this block's deferred-statement entry slices (their stmt ids were // already replayed at every path that can leave this block) and pop the frame. for i := defer_start; i < len(ctx.defers^); i += 1 { delete(ctx.defers^[i], checker.allocator) } resize(ctx.defers, defer_start) resize(ctx.locals, scope_start) } return body[:] } // build_value_block builds a `{ ... yield v }` value block whose final statement // must be a `yield`. Catch handlers may instead exit on every path, in which case // `allow_exit` leaves the fallback expression invalid. Otherwise it builds the leading // statements inline, evaluates the yield in their scope, then captures the value before // running defers. `expected` is the binding's type (INVALID for an untyped `::`). build_value_block :: proc( ctx: ^Build_Ctx, body: ^[dynamic]hir.Stmt_Id, body_stmts: []ast.Stmt_Id, expected: types.Type, span: source.Span, allow_exit := false, ) -> (value: hir.Expr_Id, value_type: types.Type) { checker := ctx.checker n := len(body_stmts) if n == 0 || checker.ast_module.statements[body_stmts[n - 1]].kind != .Yield { // Build whatever is there so inner errors (and misplaced yields) surface, then // report the missing trailing yield. inner := build_block(ctx, body_stmts) for s in inner { append(body, s) } if allow_exit && all_paths_exit(&checker.module, inner) { delete(inner, checker.allocator) return hir.INVALID_EXPR, expected } delete(inner, checker.allocator) id := source.add(checker.diagnostics, span, "a value block must end with an explicit 'yield'") ctx.problematic^ = true return invalid_hir_expr(checker, span, id), types.INVALID } scope_start := len(ctx.locals^) defer_start := len(ctx.defers^) // Leading statements keep the scope open (close=false) so the yield can still see // the block's locals; any nested `yield` hits the erroring `.Yield` switch case. leading := build_block(ctx, body_stmts[:n - 1], close = false) for s in leading { append(body, s) } delete(leading, checker.allocator) yield_stmt := checker.ast_module.statements[body_stmts[n - 1]] if yield_stmt.value_control_flow { value, value_type = build_value_source(ctx, body, yield_stmt.body, expected, yield_stmt.span, symbol.INVALID, yield_stmt.value_control_flow) } else { value = build_expr( checker, yield_stmt.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, expected, ctx.pkg, ctx.file, ) } value_type = checker.module.exprs[value].type if is_runtime_type(checker, expected) { value = coerce_expr(checker, value, expected, yield_stmt.span) value_type = checker.module.exprs[value].type } ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid // Run the block's deferred statements before the value escapes, but capture the // value first (spill to a temp) so a defer can't change what is yielded — the same // rule as `return`. if len(ctx.defers^) > defer_start { if checker.module.exprs[value].kind != .Invalid { tmp := append_tracked_local( ctx.hir_locals, ctx.local_spans, ctx.local_used, ctx.local_warnable, hir.Local{name = checker.sink_symbol, type = value_type, mutable = false}, source.Span{}, ) append(body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Declaration, span = yield_stmt.span, local = tmp, expr = value, diagnostic = source.INVALID_DIAGNOSTIC, }) value = hir.expr_id(len(checker.module.exprs)) append(&checker.module.exprs, hir.Expr{ kind = .Local, span = yield_stmt.span, type = value_type, target = hir.local_ref(tmp), }) } flush_defers(ctx, body, defer_start) } // Close the scope (build_block left it open for us). for i := defer_start; i < len(ctx.defers^); i += 1 { delete(ctx.defers^[i], checker.allocator) } resize(ctx.defers, defer_start) resize(ctx.locals, scope_start) return value, value_type } // build_value_source feeds a declaration/assignment RHS into the right value builder. // Braced blocks are plain value blocks and must end in their own `yield`; bare RHS // control flow (`x :: match ...`) sets `value_control_flow` and can produce directly. // `label` is the labeled-block label (INVALID otherwise). build_value_source :: proc( ctx: ^Build_Ctx, body: ^[dynamic]hir.Stmt_Id, body_stmts: []ast.Stmt_Id, expected: types.Type, span: source.Span, label := symbol.INVALID, value_control_flow := false, allow_exit := false, ) -> (value: hir.Expr_Id, value_type: types.Type) { checker := ctx.checker if symbol.is_valid(label) { return build_value_labeled_block(ctx, body, body_stmts, label, expected, span) } if value_control_flow && len(body_stmts) == 1 { #partial switch checker.ast_module.statements[body_stmts[0]].kind { case .If: return build_value_if(ctx, body, body_stmts[0], expected, span) case .For, .While: return build_value_loop(ctx, body, body_stmts[0], expected, span) case .Match: return build_value_match(ctx, body, body_stmts[0], expected, span) } } return build_value_block(ctx, body, body_stmts, expected, span, allow_exit) } // new_value_slot allocates a fresh, un-nameable mutable local to hold a value-if/loop // result. Branches/iterations assign it; the construct's value is a read of it. new_value_slot :: proc(ctx: ^Build_Ctx, slot_type: types.Type) -> hir.Local_Id { return append_tracked_local( ctx.hir_locals, ctx.local_spans, ctx.local_used, ctx.local_warnable, hir.Local{name = ctx.checker.sink_symbol, type = slot_type, mutable = true}, source.Span{}, ) } // slot_read builds a `.Local` read of a result slot. slot_read :: proc(checker: ^Checker, slot: hir.Local_Id, slot_type: types.Type, span: source.Span) -> hir.Expr_Id { id := hir.expr_id(len(checker.module.exprs)) append(&checker.module.exprs, hir.Expr{ kind = .Local, span = span, type = slot_type, target = hir.local_ref(slot), }) return id } // emit_slot_assign appends a bare-local `slot = value` assignment to `out`. emit_slot_assign :: proc(checker: ^Checker, out: ^[dynamic]hir.Stmt_Id, slot: hir.Local_Id, value: hir.Expr_Id, span: source.Span) { append(out, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Assignment, span = span, expr = value, local = slot, target = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC, }) } string_peer_slice_type :: proc(checker: ^Checker, value: types.Type) -> types.Type { pointer, array, ok := types.array_pointer(value, &checker.module.types) if ok && !pointer.mutable && !array.mutable && array.child == types.U8 && array.has_sentinel && array.sentinel == 0 { return types.slice(&checker.module.types, types.U8, false, true, 0) } return types.INVALID } adopt_value_slot :: proc( ctx: ^Build_Ctx, slot: ^hir.Local_Id, slot_type: ^types.Type, value: hir.Expr_Id, vtype: types.Type, span: source.Span, ) -> hir.Expr_Id { checker := ctx.checker if slot^ == hir.INVALID_LOCAL { peer := string_peer_slice_type(checker, vtype) if types.is_valid(peer) { slot_type^ = peer slot^ = new_value_slot(ctx, slot_type^) return coerce_expr(checker, value, slot_type^, span) } slot_type^ = vtype slot^ = new_value_slot(ctx, slot_type^) return value } return coerce_expr(checker, value, slot_type^, span) } // build_value_if turns `if c { … yield A } else { … yield B }` into a result slot // each branch assigns, read after the if. Every path must yield: a mandatory `else`, // each branch ends in `yield`, and all branches share a type (the first establishes it // when untyped; later branches coerce). HIR holds an ordinary `.If` + a `.Local` read. build_value_if :: proc( ctx: ^Build_Ctx, body: ^[dynamic]hir.Stmt_Id, if_id: ast.Stmt_Id, expected: types.Type, span: source.Span, ) -> (value: hir.Expr_Id, value_type: types.Type) { checker := ctx.checker slot := hir.INVALID_LOCAL slot_type := types.INVALID if is_runtime_type(checker, expected) { slot_type = expected slot = new_value_slot(ctx, slot_type) } subtree: [dynamic]hir.Stmt_Id subtree.allocator = checker.allocator ok := emit_value_if(ctx, &subtree, if_id, &slot, &slot_type, span) if !ok || slot == hir.INVALID_LOCAL { for s in subtree { append(body, s) } delete(subtree) ctx.problematic^ = true return invalid_hir_expr(checker, span, source.INVALID_DIAGNOSTIC), types.INVALID } // The slot's poison declaration precedes the if; every path assigns it. append(body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Declaration, span = span, local = slot, expr = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC, }) for s in subtree { append(body, s) } delete(subtree) value = slot_read(checker, slot, slot_type, span) return value, slot_type } // emit_value_if builds one `if`/`else if`/`else` level of a value-if, appending the // assembled `.If` to `out`. `slot`/`slot_type` thread through so the first branch can // fix an untyped slot and `else if` chains share it. emit_value_if :: proc( ctx: ^Build_Ctx, out: ^[dynamic]hir.Stmt_Id, if_id: ast.Stmt_Id, slot: ^hir.Local_Id, slot_type: ^types.Type, span: source.Span, ) -> bool { checker := ctx.checker if_stmt := checker.ast_module.statements[if_id] condition := hir.INVALID_EXPR guard := hir.INVALID_EXPR unwraps: []hir.Conditional_Unwrap = nil capture_start := len(ctx.locals^) if len(if_stmt.captures) > 0 { // Unwrap value-if (`name :: if opt |v| { yield v } else { yield 0 }`): mirror the // build-pass unwrap arm to bind captures + guard; each branch then assigns the slot // like any other branch, and the HIR `.If` carries the unwraps (lowering handles it). ast_operands: [dynamic]ast.Expr_Id ast_operands.allocator = checker.allocator flatten_conditional_unwrap_operands(checker.ast_module, if_stmt.expr, &ast_operands) ok := true if len(ast_operands) != len(if_stmt.captures) { source.addf(checker.diagnostics, if_stmt.span, "'if' unwrap has %d operands but %d captures", len(ast_operands), len(if_stmt.captures)) ok = false } values := make([]hir.Expr_Id, len(ast_operands), checker.allocator) child_types := make([]types.Type, len(ast_operands), checker.allocator) for operand, index in ast_operands { child_types[index] = types.INVALID v := build_expr(checker, operand, ctx.locals^[:], ctx.global_reads, ctx.calls, types.INVALID, ctx.pkg, ctx.file) values[index] = v vt := checker.module.exprs[v].type if checker.module.exprs[v].kind == .Invalid { ok = false } else if !types.is_optional(vt, &checker.module.types) { source.addf(checker.diagnostics, checker.ast_module.exprs[operand].span, "'if' unwrap requires an optional value (operand %d)", index + 1) ok = false } else { child_types[index] = types.child_type(vt, &checker.module.types) } } unwrap_list: [dynamic]hir.Conditional_Unwrap unwrap_list.allocator = checker.allocator for capture, index in if_stmt.captures { child := child_types[index] if index < len(child_types) else types.INVALID local := hir.INVALID_LOCAL if capture != checker.sink_symbol { if _, dup := find_build_local(ctx.locals^[capture_start:], capture); dup { source.add(checker.diagnostics, if_stmt.span, "'if' unwrap captures must have distinct names") ok = false } else if add_shadow_diagnostic( checker, if_stmt.span, capture, "capture", ctx.pkg, ctx.file, ctx.locals^[:capture_start], ctx.loop_labels^[:], ctx.yield_targets^[:], ) != source.INVALID_DIAGNOSTIC { ok = false } local = append_build_local(ctx, capture, child, false, if_stmt.span) } if index < len(values) { append(&unwrap_list, hir.Conditional_Unwrap{expr=values[index], local=local}) } } if if_stmt.guard != ast.INVALID_EXPR { guard = build_expr(checker, if_stmt.guard, ctx.locals^[:], ctx.global_reads, ctx.calls, types.BOOL, ctx.pkg, ctx.file) if checker.module.exprs[guard].kind == .Invalid { ok = false } else if !types.is_bool(checker.module.exprs[guard].type) { source.add(checker.diagnostics, checker.ast_module.exprs[if_stmt.guard].span, "'if' unwrap guard must be a bool") ok = false } } delete(values, checker.allocator) delete(child_types, checker.allocator) delete(ast_operands) if !ok { resize(ctx.locals, capture_start) delete(unwrap_list) ctx.problematic^ = true return false } unwraps = unwrap_list[:] } else { condition = build_expr(checker, if_stmt.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, types.BOOL, ctx.pkg, ctx.file) if checker.module.exprs[condition].kind != .Invalid && !types.is_bool(checker.module.exprs[condition].type) { id := source.add(checker.diagnostics, if_stmt.span, "'if' condition must be a bool") condition = invalid_hir_expr(checker, if_stmt.span, id, types.BOOL) ctx.problematic^ = true } } // then-branch (unwrap captures, if any, are in scope here, then dropped before else). then_body: [dynamic]hir.Stmt_Id then_body.allocator = checker.allocator then_ok := emit_value_branch(ctx, &then_body, if_stmt.body, slot, slot_type, span) resize(ctx.locals, capture_start) if !then_ok { delete(then_body) delete(unwraps) return false } if if_stmt.else_body == nil { source.add(checker.diagnostics, if_stmt.span, "an 'if' used as a value must have an 'else' so every path yields") delete(then_body) delete(unwraps) ctx.problematic^ = true return false } else_body: [dynamic]hir.Stmt_Id else_body.allocator = checker.allocator branch_ok := true if len(if_stmt.else_body) == 1 && checker.ast_module.statements[if_stmt.else_body[0]].kind == .If { branch_ok = emit_value_if(ctx, &else_body, if_stmt.else_body[0], slot, slot_type, span) } else { branch_ok = emit_value_branch(ctx, &else_body, if_stmt.else_body, slot, slot_type, span) } if !branch_ok { delete(then_body) delete(else_body) delete(unwraps) return false } append(out, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .If, span = if_stmt.span, expr = condition, guard = guard, unwraps = unwraps, then_body = then_body[:], else_body = else_body[:], local = hir.INVALID_LOCAL, target = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC, }) return true } // emit_value_branch builds one branch of a value-if as a value block (leading stmts + // trailing yield) and appends `slot = `. The first branch of an untyped value-if // fixes the slot type; later branches coerce to it (a mismatch is the "same type" error). // A branch that does not yield is valid only if it exits on every path (return/break/ // continue) — it then produces no value and never reaches the slot read. emit_value_branch :: proc( ctx: ^Build_Ctx, out: ^[dynamic]hir.Stmt_Id, branch_stmts: []ast.Stmt_Id, slot: ^hir.Local_Id, slot_type: ^types.Type, span: source.Span, ) -> bool { checker := ctx.checker n := len(branch_stmts) ends_in_yield := n > 0 && checker.ast_module.statements[branch_stmts[n - 1]].kind == .Yield && !symbol.is_valid(checker.ast_module.statements[branch_stmts[n - 1]].label) if !ends_in_yield { // Not a value block: only allowed if every path exits (e.g. `else { return -1 }`), // in which case it contributes no value to the slot. built := build_block(ctx, branch_stmts) for s in built { append(out, s) } terminates := all_paths_exit(&checker.module, built) delete(built, checker.allocator) if terminates { return true } source.add(checker.diagnostics, span, "a value branch must end with 'yield' or exit on every path (return/break/continue)") ctx.problematic^ = true return false } value, vtype := build_value_block(ctx, out, branch_stmts, slot_type^, span) if checker.module.exprs[value].kind == .Invalid { return false } value = adopt_value_slot(ctx, slot, slot_type, value, vtype, span) emit_slot_assign(checker, out, slot^, value, span) return true } // Match_Built_Arm holds one already-built arm: its dispatch condition (`INVALID_EXPR` // for the terminal `else`/exhaustive arm) and its body statements. The chain is // assembled backward from these so diagnostics stay in source order. Match_Built_Arm :: struct { condition: hir.Expr_Id, body: []hir.Stmt_Id, terminal: bool, } // match_subject_location yields a fresh location expr for the `match` subject: a direct // read of the value temp, or a deref of the pointer temp when an arm pointer-captures (so // captures alias the original storage). Either way its lowered address is the subject's. match_subject_location :: proc(checker: ^Checker, subj_local: hir.Local_Id, is_pointer: bool, subject_type, ptr_type: types.Type, span: source.Span) -> hir.Expr_Id { if !is_pointer { return slot_read(checker, subj_local, subject_type, span) } return add_hir_expr(checker, hir.Expr{ kind = .Deref, span = span, type = subject_type, left = slot_read(checker, subj_local, ptr_type, span), target = hir.INVALID_REF, right = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC, }) } // match_or ORs a fresh dispatch comparison into an arm's accumulating condition (for a // multi-pattern arm), or returns it directly for the first pattern. match_or :: proc(checker: ^Checker, condition, cmp: hir.Expr_Id, span: source.Span) -> hir.Expr_Id { if condition == hir.INVALID_EXPR { return cmp } return add_hir_expr(checker, hir.Expr{ kind = .Or, span = span, type = types.BOOL, left = condition, right = cmp, target = hir.INVALID_REF, diagnostic = source.INVALID_DIAGNOSTIC, }) } // emit_match desugars a `match` into a single subject spill, one dispatch key read, and // an `if`/`else if` chain. `as_value` (with `slot`/`slot_type`) routes each arm body // through the value-branch machinery so the construct produces a value; otherwise arm // bodies are ordinary statement blocks. Returns false (and emits a `.Trap`) on any error. emit_match :: proc( ctx: ^Build_Ctx, out: ^[dynamic]hir.Stmt_Id, statement: ast.Stmt, as_value: bool, slot: ^hir.Local_Id, slot_type: ^types.Type, ) -> bool { checker := ctx.checker store := &checker.module.types span := statement.span fail :: proc(ctx: ^Build_Ctx, out: ^[dynamic]hir.Stmt_Id, span: source.Span, diagnostic: source.Diagnostic_Id) -> bool { checker := ctx.checker append(out, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Trap, span = span, expr = hir.INVALID_EXPR, local = hir.INVALID_LOCAL, diagnostic = diagnostic, }) ctx.problematic^ = true return false } // 1. Subject. A pointer capture (`|@cap|`) must alias the original storage, so when // any arm requests one we spill the subject's *address* (it must be an addressable // lvalue) and route reads through a deref; otherwise we spill the value as a copy. // Either spill evaluates the subject exactly once. subject := build_expr(checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, types.INVALID, ctx.pkg, ctx.file) subject_type := checker.module.exprs[subject].type if checker.module.exprs[subject].kind == .Invalid { return fail(ctx, out, span, checker.module.exprs[subject].diagnostic) } is_tagged := types.is_tagged_union(subject_type, store) is_enum_subject := types.is_enum(subject_type, store) if types.is_union(subject_type, store) && !is_tagged { return fail(ctx, out, span, source.add(checker.diagnostics, span, "cannot 'match' on an untagged union; it has no tag to dispatch on")) } if !is_tagged && !is_enum_subject && !types.is_concrete_scalar(subject_type) { return fail(ctx, out, span, source.addf(checker.diagnostics, span, "'match' subject must be a tagged union, enum, or scalar value, not '%s'", type_label(checker, subject_type))) } ok := true wants_pointer := false for arm_id in statement.body { arm := checker.ast_module.statements[arm_id] if arm.kind == .Match_Arm && arm.pointer_capture { wants_pointer = true break } } subj_is_pointer := false subj_writable := false ptr_type := types.INVALID if wants_pointer && is_tagged { if hir_is_location(checker, subject) { subj_is_pointer = true subj_writable = hir_location_writable(checker, subject, ctx.locals^[:]) ptr_type = types.pointer(store, subject_type, subj_writable, false) } else { source.add(checker.diagnostics, span, "a pointer capture requires an addressable 'match' subject (bind it to a variable first)") ok = false } } spill_type := ptr_type if subj_is_pointer else subject_type spill_value := subject if subj_is_pointer { spill_value = add_hir_expr(checker, hir.Expr{ kind = .Address, span = span, type = ptr_type, left = subject, target = hir.INVALID_REF, right = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC, }) } subj_local := append_tracked_local( ctx.hir_locals, ctx.local_spans, ctx.local_used, ctx.local_warnable, hir.Local{name = checker.sink_symbol, type = spill_type, mutable = false}, source.Span{}, ) append(out, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Declaration, span = span, local = subj_local, expr = spill_value, diagnostic = source.INVALID_DIAGNOSTIC, }) // 2. Dispatch key: a tagged union reads its discriminant into its own temp; an enum // or scalar compares the subject directly. key_local := subj_local key_type := spill_type tag_enum := types.INVALID if is_tagged { tag_enum = types.union_tag_enum(subject_type, store) tag_read := add_hir_expr(checker, hir.Expr{ kind = .Union_Tag, span = span, type = tag_enum, left = match_subject_location(checker, subj_local, subj_is_pointer, subject_type, ptr_type, span), target = hir.INVALID_REF, right = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC, }) tag_local := append_tracked_local( ctx.hir_locals, ctx.local_spans, ctx.local_used, ctx.local_warnable, hir.Local{name = checker.sink_symbol, type = tag_enum, mutable = false}, source.Span{}, ) append(out, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Declaration, span = span, local = tag_local, expr = tag_read, diagnostic = source.INVALID_DIAGNOSTIC, }) key_local = tag_local key_type = tag_enum } // 3. Build each arm (forward, for source-order diagnostics). An arm's `patterns` may // list several alternatives (`.a, .b:` / `0, 1:`); their conditions are OR'd. built: [dynamic]Match_Built_Arm built.allocator = checker.allocator defer delete(built) covered: [dynamic]symbol.Id covered.allocator = checker.allocator defer delete(covered) has_else := false for arm_id in statement.body { arm := checker.ast_module.statements[arm_id] if arm.kind != .Match_Arm { ok = false continue } if has_else { source.add(checker.diagnostics, arm.span, "arms after 'else' are unreachable") ok = false } is_else := len(arm.patterns) == 0 condition := hir.INVALID_EXPR field_index := -1 payload_type := types.INVALID has_capture := len(arm.captures) > 0 if is_else { if has_capture { source.add(checker.diagnostics, arm.span, "the 'else' arm cannot capture a payload") ok = false } has_else = true } else if is_tagged || is_enum_subject { // The capture payload (if any) must be one type across every listed variant. capture_field := -1 capture_payload := types.INVALID capture_conflict := types.INVALID member_enum := tag_enum if is_tagged else subject_type for pat_id in arm.patterns { pattern := checker.ast_module.exprs[pat_id] if pattern.kind == .Range { source.add(checker.diagnostics, arm.span, "range patterns only apply to scalar 'match' subjects") ok = false continue } if pattern.kind != .Enum_Literal { source.add(checker.diagnostics, arm.span, "an enum or tagged-union 'match' arm must be a '.variant' pattern") ok = false continue } if contains_name(covered[:], pattern.name) { source.addf(checker.diagnostics, arm.span, "duplicate 'match' arm for '.%s'", symbol_text(checker, pattern.name)) ok = false } else { append(&covered, pattern.name) } if is_tagged { index, field, found := find_struct_field(checker, subject_type, pattern.name) if !found { source.addf(checker.diagnostics, arm.span, "unknown variant '.%s' on '%s'", symbol_text(checker, pattern.name), type_label(checker, subject_type)) ok = false continue } if capture_field < 0 { capture_field = index capture_payload = field.type } else if !types.equal(capture_payload, field.type) { capture_conflict = field.type } } else { if _, found := find_enum_member(checker, subject_type, pattern.name); !found { source.addf(checker.diagnostics, arm.span, "unknown member '.%s' on '%s'", symbol_text(checker, pattern.name), type_label(checker, subject_type)) ok = false continue } } member := enum_member_hir(checker, member_enum, pattern.name, arm.span) cmp := add_hir_expr(checker, hir.Expr{ kind = .Eq, span = arm.span, type = types.BOOL, left = slot_read(checker, key_local, key_type, arm.span), right = member, target = hir.INVALID_REF, diagnostic = source.INVALID_DIAGNOSTIC, }) condition = match_or(checker, condition, cmp, arm.span) } if has_capture { if !is_tagged { source.add(checker.diagnostics, arm.span, "only tagged-union variants can capture a payload") ok = false } else if types.is_valid(capture_conflict) { source.addf(checker.diagnostics, arm.span, "capture group with incompatible types '%s' and '%s'", type_label(checker, capture_payload), type_label(checker, capture_conflict)) ok = false } else if types.is_void(capture_payload) { source.add(checker.diagnostics, arm.span, "this variant has a void payload; there is nothing to capture") ok = false } else { field_index = capture_field payload_type = capture_payload } } } else { // Scalar subject: each pattern is a literal or a range, compared to the subject. if has_capture { source.add(checker.diagnostics, arm.span, "only tagged-union variants can capture a payload") ok = false } for pat_id in arm.patterns { pat_ast := checker.ast_module.exprs[pat_id] cmp := hir.INVALID_EXPR if pat_ast.kind == .Range { lo := build_expr(checker, pat_ast.left, ctx.locals^[:], ctx.global_reads, ctx.calls, subject_type, ctx.pkg, ctx.file) lo = coerce_expr(checker, lo, subject_type, arm.span) hi := build_expr(checker, pat_ast.right, ctx.locals^[:], ctx.global_reads, ctx.calls, subject_type, ctx.pkg, ctx.file) hi = coerce_expr(checker, hi, subject_type, arm.span) if checker.module.exprs[lo].kind == .Invalid || checker.module.exprs[hi].kind == .Invalid { ok = false continue } ge := add_hir_expr(checker, hir.Expr{ kind = .Ge, span = arm.span, type = types.BOOL, left = slot_read(checker, key_local, key_type, arm.span), right = lo, target = hir.INVALID_REF, diagnostic = source.INVALID_DIAGNOSTIC, }) hi_cmp := add_hir_expr(checker, hir.Expr{ kind = .Le if pat_ast.integer == 1 else .Lt, span = arm.span, type = types.BOOL, left = slot_read(checker, key_local, key_type, arm.span), right = hi, target = hir.INVALID_REF, diagnostic = source.INVALID_DIAGNOSTIC, }) cmp = add_hir_expr(checker, hir.Expr{ kind = .And, span = arm.span, type = types.BOOL, left = ge, right = hi_cmp, target = hir.INVALID_REF, diagnostic = source.INVALID_DIAGNOSTIC, }) } else { pattern := build_expr(checker, pat_id, ctx.locals^[:], ctx.global_reads, ctx.calls, subject_type, ctx.pkg, ctx.file) pattern = coerce_expr(checker, pattern, subject_type, arm.span) if checker.module.exprs[pattern].kind == .Invalid { ok = false continue } cmp = add_hir_expr(checker, hir.Expr{ kind = .Eq, span = arm.span, type = types.BOOL, left = slot_read(checker, key_local, key_type, arm.span), right = pattern, target = hir.INVALID_REF, diagnostic = source.INVALID_DIAGNOSTIC, }) } condition = match_or(checker, condition, cmp, arm.span) } } arm_body, body_ok := build_match_arm_body(ctx, arm, subject_type, subj_local, subj_is_pointer, ptr_type, subj_writable, field_index, payload_type, as_value, slot, slot_type, span) if !body_ok { ok = false } append(&built, Match_Built_Arm{condition = condition, body = arm_body, terminal = is_else}) } // 4. Exhaustiveness. Enum/union matches must cover every variant or supply `else`; // an already-exhaustive match must not carry a redundant `else`. The last // covered arm is promoted to the unconditional `else` so the chain terminates. if is_tagged || is_enum_subject { all_names := types.enum_members_for(store, tag_enum) if is_tagged else types.enum_members_for(store, subject_type) names: [dynamic]symbol.Id names.allocator = checker.allocator defer delete(names) if is_tagged { for field in types.fields_for(store, subject_type) { append(&names, symbol.Id(field.name)) } } else { for member in all_names { append(&names, symbol.Id(member.name)) } } missing: [dynamic]symbol.Id missing.allocator = checker.allocator defer delete(missing) for name in names { if !contains_name(covered[:], name) { append(&missing, name) } } if has_else { if len(missing) == 0 { source.add(checker.diagnostics, span, "redundant 'else': the 'match' already covers every variant") ok = false } } else if len(missing) > 0 { builder: strings.Builder strings.builder_init(&builder, checker.allocator) defer strings.builder_destroy(&builder) for name, index in missing { if index > 0 { strings.write_string(&builder, ", ") } strings.write_string(&builder, ".") strings.write_string(&builder, symbol_text(checker, name)) } source.addf(checker.diagnostics, span, "'match' on '%s' is not exhaustive; missing variants: %s (add the arms or an 'else')", type_label(checker, subject_type), strings.to_string(builder)) ok = false } else if len(built) > 0 { built[len(built) - 1].terminal = true } } else if !has_else { source.addf(checker.diagnostics, span, "a 'match' on '%s' requires an 'else' arm", type_label(checker, subject_type)) ok = false } if !ok { // The arm bodies never get wired into the (un-assembled) chain, so free them here. for arm in built { delete(arm.body, checker.allocator) } return fail(ctx, out, span, source.INVALID_DIAGNOSTIC) } // 5. Assemble the if/else chain backward from the built arms. The terminal arm is the // final (else / promoted) one; the rest nest as `if cond { body } else { … }`. else_chain: []hir.Stmt_Id = nil start := len(built) if len(built) > 0 && built[len(built) - 1].terminal { else_chain = built[len(built) - 1].body start = len(built) - 1 } for i := start - 1; i >= 0; i -= 1 { arm := built[i] wrapper := make([]hir.Stmt_Id, 1, checker.allocator) wrapper[0] = hir.stmt_id(len(checker.module.statements)) append(&checker.module.statements, hir.Stmt{ kind = .If, span = span, expr = arm.condition, guard = hir.INVALID_EXPR, then_body = arm.body, else_body = else_chain, local = hir.INVALID_LOCAL, target = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC, }) else_chain = wrapper } for s in else_chain { append(out, s) } // The outermost chain slice's ids are now copied into `out`; the inner slices are // owned by their enclosing `.If` (freed with the HIR module). delete(else_chain, checker.allocator) return true } // build_match_arm_body builds one arm's body, prefixed with the optional payload capture // (`cap := subject.variant`, an unchecked reinterpret like a Zig union field read). For a // statement match it is a plain block; for a value match each path assigns the result slot // (a single-expression arm yields implicitly). build_match_arm_body :: proc( ctx: ^Build_Ctx, arm: ast.Stmt, subject_type: types.Type, subj_local: hir.Local_Id, subj_is_pointer: bool, ptr_type: types.Type, subj_writable: bool, field_index: int, payload_type: types.Type, as_value: bool, slot: ^hir.Local_Id, slot_type: ^types.Type, span: source.Span, ) -> ([]hir.Stmt_Id, bool) { checker := ctx.checker result: [dynamic]hir.Stmt_Id result.allocator = checker.allocator capture_start := len(ctx.locals^) capture_ok := true if len(arm.captures) > 0 && field_index >= 0 { capture := arm.captures[0] if capture != checker.sink_symbol { if id := add_shadow_diagnostic( checker, span, capture, "capture", ctx.pkg, ctx.file, ctx.locals^[:], ctx.loop_labels^[:], ctx.yield_targets^[:], ); id != source.INVALID_DIAGNOSTIC { append(&result, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Trap, span = span, expr = hir.INVALID_EXPR, local = hir.INVALID_LOCAL, diagnostic = id, }) capture_ok = false } // The payload sits at the subject's shared carrier offset. A value capture // loads it; a `|@cap|` capture binds a pointer to it (mutability follows the // subject), aliasing the original storage via the subject location. field_read := add_hir_expr(checker, hir.Expr{ kind = .Field, span = span, type = payload_type, integer = i64(field_index), left = match_subject_location(checker, subj_local, subj_is_pointer, subject_type, ptr_type, span), target = hir.INVALID_REF, right = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC, }) cap_type := payload_type cap_value := field_read if arm.pointer_capture { cap_type = types.pointer(&checker.module.types, payload_type, subj_writable, false) cap_value = add_hir_expr(checker, hir.Expr{ kind = .Address, span = span, type = cap_type, left = field_read, target = hir.INVALID_REF, right = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC, }) } cap_local := append_build_local(ctx, capture, cap_type, false, span) append(&result, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Declaration, span = span, local = cap_local, expr = cap_value, diagnostic = source.INVALID_DIAGNOSTIC, }) } } body_ok := capture_ok if !as_value { built := build_block(ctx, arm.body) for s in built { append(&result, s) } delete(built, checker.allocator) } else { body_ok = build_value_arm(ctx, &result, arm.body, slot, slot_type, span) && body_ok } resize(ctx.locals, capture_start) return result[:], body_ok } // build_value_arm appends a value-match arm's slot assignment(s) to `out`: a single bare // expression yields implicitly; anything else reuses the value-branch rule (trailing // `yield`, or exit on every path). build_value_arm :: proc( ctx: ^Build_Ctx, out: ^[dynamic]hir.Stmt_Id, arm_body: []ast.Stmt_Id, slot: ^hir.Local_Id, slot_type: ^types.Type, span: source.Span, ) -> bool { checker := ctx.checker if len(arm_body) == 1 && checker.ast_module.statements[arm_body[0]].kind == .Expression { expr_stmt := checker.ast_module.statements[arm_body[0]] expected := slot_type^ if slot^ != hir.INVALID_LOCAL else types.INVALID value := build_expr(checker, expr_stmt.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, expected, ctx.pkg, ctx.file) if checker.module.exprs[value].kind == .Invalid { ctx.problematic^ = true return false } vtype := checker.module.exprs[value].type value = adopt_value_slot(ctx, slot, slot_type, value, vtype, span) emit_slot_assign(checker, out, slot^, value, span) return true } return emit_value_branch(ctx, out, arm_body, slot, slot_type, span) } // build_match desugars a statement-position `match` into its if/else chain. build_match :: proc(ctx: ^Build_Ctx, body: ^[dynamic]hir.Stmt_Id, statement: ast.Stmt) { slot := hir.INVALID_LOCAL slot_type := types.INVALID emit_match(ctx, body, statement, false, &slot, &slot_type) } // build_value_match desugars a `match` used as a declaration/assignment RHS: a result slot // each arm assigns, read after the chain. Mirrors build_value_if. build_value_match :: proc( ctx: ^Build_Ctx, body: ^[dynamic]hir.Stmt_Id, match_id: ast.Stmt_Id, expected: types.Type, span: source.Span, ) -> (value: hir.Expr_Id, value_type: types.Type) { checker := ctx.checker statement := checker.ast_module.statements[match_id] slot := hir.INVALID_LOCAL slot_type := types.INVALID if is_runtime_type(checker, expected) { slot_type = expected slot = new_value_slot(ctx, slot_type) } subtree: [dynamic]hir.Stmt_Id subtree.allocator = checker.allocator ok := emit_match(ctx, &subtree, statement, true, &slot, &slot_type) if !ok || slot == hir.INVALID_LOCAL { for s in subtree { append(body, s) } delete(subtree) ctx.problematic^ = true return invalid_hir_expr(checker, span, source.INVALID_DIAGNOSTIC), types.INVALID } append(body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Declaration, span = span, local = slot, expr = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC, }) for s in subtree { append(body, s) } delete(subtree) return slot_read(checker, slot, slot_type, span), slot_type } // loop_yields_none reports whether any `yield` that targets this loop (a labeled // `yield :blk` inside `if`/block branches, or the trailing fall-through) yields the // literal `none` — making the loop's result optional. Pure AST walk; does not descend // into nested loops or value sources, whose yields belong to them. loop_yields_none :: proc(checker: ^Checker, stmts: []ast.Stmt_Id) -> bool { for id in stmts { s := checker.ast_module.statements[id] #partial switch s.kind { case .Yield: if s.expr != ast.INVALID_EXPR && checker.ast_module.exprs[s.expr].kind == .None { return true } case .If: if loop_yields_none(checker, s.body) || loop_yields_none(checker, s.else_body) { return true } case .Block: if loop_yields_none(checker, s.body) { return true } } } return false } // resolve_loop_slot fixes a value-loop's result slot from its first concrete yield (an // optional element type when the loop also yields `none`) and coerces `value` into it. // Returns INVALID when the type can't be fixed yet (a `none`/invalid first yield). resolve_loop_slot :: proc(ctx: ^Build_Ctx, target: ^Yield_Target, value: hir.Expr_Id, vtype: types.Type, span: source.Span) -> hir.Expr_Id { checker := ctx.checker if target.slot == hir.INVALID_LOCAL { if !is_runtime_type(checker, vtype) { return hir.INVALID_EXPR } target.slot_type = types.optional(&checker.module.types, vtype) if target.result_optional else vtype target.slot = new_value_slot(ctx, target.slot_type) } return coerce_expr(checker, value, target.slot_type, span) } // first_concrete_yield_expr returns the AST expr of the first yield (source order) that is // not the literal `none`, descending into `if`/block branches but not nested loops or value // sources (whose yields belong to them). INVALID when the loop yields only `none`. first_concrete_yield_expr :: proc(checker: ^Checker, stmts: []ast.Stmt_Id) -> ast.Expr_Id { for id in stmts { s := checker.ast_module.statements[id] #partial switch s.kind { case .Yield: if s.expr != ast.INVALID_EXPR && checker.ast_module.exprs[s.expr].kind != .None { return s.expr } case .If: if e := first_concrete_yield_expr(checker, s.body); e != ast.INVALID_EXPR { return e } if e := first_concrete_yield_expr(checker, s.else_body); e != ast.INVALID_EXPR { return e } case .Block: if e := first_concrete_yield_expr(checker, s.body); e != ast.INVALID_EXPR { return e } } } return ast.INVALID_EXPR } // value_loop_element_type pre-types the element of an untyped value loop from its first // concrete (non-`none`) yield, so a `none` yielded before any concrete value still resolves // the result to `?T`. The loop's captures are bound temporarily for the probe and the probe // expr is discarded; returns INVALID when the loop yields only `none`. value_loop_element_type :: proc(ctx: ^Build_Ctx, loop_stmt: ast.Stmt) -> types.Type { checker := ctx.checker yield_expr := first_concrete_yield_expr(checker, loop_stmt.body) if yield_expr == ast.INVALID_EXPR { return types.INVALID } capture_start := len(ctx.locals^) local_start := len(ctx.hir_locals^) if loop_stmt.kind == .For { // Mirror the `.For` arm's capture-type computation just enough to type the probe. iterable := build_expr(checker, loop_stmt.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, types.INVALID, ctx.pkg, ctx.file) iterable_type := checker.module.exprs[iterable].type capture_type := types.INVALID if types.is_range(iterable_type, &checker.module.types) { capture_type = types.child_type(iterable_type, &checker.module.types) } else if item, ok := sequence_item(iterable_type, &checker.module.types); ok { capture_type = item.child if loop_stmt.pointer_capture { capture_type = types.pointer(&checker.module.types, item.child, item.mutable, false) } } if symbol.is_valid(loop_stmt.name) { append_build_local(ctx, loop_stmt.name, capture_type, false, loop_stmt.span) } if symbol.is_valid(loop_stmt.index_name) { append_build_local(ctx, loop_stmt.index_name, types.USIZE, false, loop_stmt.span) } } probe := build_expr(checker, yield_expr, ctx.locals^[:], ctx.global_reads, ctx.calls, types.INVALID, ctx.pkg, ctx.file) result := checker.module.exprs[probe].type if checker.module.exprs[probe].kind != .Invalid else types.INVALID resize(ctx.locals, capture_start) ignore_tracked_locals(ctx, local_start) return result } // stmt_contains_yield reports whether a statement contains a `yield` anywhere within it // (recursing through if/block/loop bodies). Used to stop the leading probe build before any // statement that yields (the block's yield target is not pushed during the probe). stmt_contains_yield :: proc(checker: ^Checker, id: ast.Stmt_Id) -> bool { s := checker.ast_module.statements[id] #partial switch s.kind { case .Yield: return true case .If, .For, .While, .Block: for sub in s.body { if stmt_contains_yield(checker, sub) { return true } } for sub in s.else_body { if stmt_contains_yield(checker, sub) { return true } } } return false } // block_element_type pre-types an untyped value block's element from its first concrete // `yield :blk`, building the block's leading (yield-free) statements first so the probe can // reference block locals declared before the first yield. The leading build is a throwaway // (its scope is restored). INVALID when there is no concrete yield, or the concrete yield // references a local only in scope past the first yield (annotate the binding instead). block_element_type :: proc(ctx: ^Build_Ctx, block_stmts: []ast.Stmt_Id) -> types.Type { checker := ctx.checker concrete := first_concrete_yield_expr(checker, block_stmts) if concrete == ast.INVALID_EXPR { return types.INVALID } lead_end := len(block_stmts) for id, i in block_stmts { if stmt_contains_yield(checker, id) { lead_end = i break } } scope_start := len(ctx.locals^) local_start := len(ctx.hir_locals^) defer_start := len(ctx.defers^) lead := build_block(ctx, block_stmts[:lead_end], close = false) delete(lead, checker.allocator) probe := build_expr(checker, concrete, ctx.locals^[:], ctx.global_reads, ctx.calls, types.INVALID, ctx.pkg, ctx.file) result := checker.module.exprs[probe].type if checker.module.exprs[probe].kind != .Invalid else types.INVALID // Discard the throwaway leading build's scope (its hir stmts/locals are dead but stable). for i := defer_start; i < len(ctx.defers^); i += 1 { delete(ctx.defers^[i], checker.allocator) } resize(ctx.defers, defer_start) resize(ctx.locals, scope_start) ignore_tracked_locals(ctx, local_start) return result } // build_value_labeled_block turns `x :: blk: { …; yield :blk v }` into a result slot each // `yield :blk` assigns (via the build_block `.Yield` desugar → `slot = v; break :blk`), then // reads it after the block. Every path must yield (or otherwise exit); HIR holds a `.Block` // that emits the body and the exit label the labeled breaks branch to. No iteration / no // fall-through (unlike a value loop). The type is the annotation when typed, else the first // concrete `yield :blk`'s type (optional when any yield is `none`). build_value_labeled_block :: proc( ctx: ^Build_Ctx, body: ^[dynamic]hir.Stmt_Id, block_stmts: []ast.Stmt_Id, label: symbol.Id, expected: types.Type, span: source.Span, ) -> (value: hir.Expr_Id, value_type: types.Type) { checker := ctx.checker result_optional := loop_yields_none(checker, block_stmts) slot := hir.INVALID_LOCAL slot_type := types.INVALID if is_runtime_type(checker, expected) { slot_type = expected slot = new_value_slot(ctx, slot_type) result_optional = types.is_optional(slot_type, &checker.module.types) } else if result_optional { // Untyped block that also yields `none`: pre-type the element from the first // concrete yield (regardless of source order) so a `none` yielded first still // resolves the result to `?T`. elem := block_element_type(ctx, block_stmts) if is_runtime_type(checker, elem) { slot_type = types.optional(&checker.module.types, elem) slot = new_value_slot(ctx, slot_type) } } if id := add_label_shadow_diagnostic(ctx, span, label); id != source.INVALID_DIAGNOSTIC { ctx.problematic^ = true return invalid_hir_expr(checker, span, id), types.INVALID } append(ctx.yield_targets, Yield_Target{ label = label, slot = slot, slot_type = slot_type, result_optional = result_optional, defer_floor = len(ctx.defers^), }) built := build_block(ctx, block_stmts) target := pop(ctx.yield_targets) if target.slot == hir.INVALID_LOCAL { for s in built { append(body, s) } delete(built, checker.allocator) id := source.add(checker.diagnostics, span, "could not determine the value block's yield type; annotate the binding") ctx.problematic^ = true return invalid_hir_expr(checker, span, id), types.INVALID } // Every path must `yield :blk` (or return/break out); otherwise a path falls through // to the slot read with a poison value. if !all_paths_exit(&checker.module, built) { for s in built { append(body, s) } delete(built, checker.allocator) id := source.add(checker.diagnostics, span, "a labeled value block must 'yield' on every path") ctx.problematic^ = true return invalid_hir_expr(checker, span, id), types.INVALID } append(body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Declaration, span = span, local = target.slot, expr = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC, }) append(body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Block, span = span, label = label, then_body = built, local = hir.INVALID_LOCAL, target = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC, }) value = slot_read(checker, target.slot, target.slot_type, span) return value, target.slot_type } // build_value_loop turns a labeled `for/while ... blk: { … }` whose body ends in a // fall-through `yield` (and may early-exit via `yield :blk x`) into a result slot: // the fall-through value initializes the slot before the loop, each `yield :blk x` // desugars (in build_block) to `slot = x; break`, and the construct's value is a read // of the slot after the loop. Reuses the ordinary `.For`/`.While` build via a peeled // copy; no new HIR. The yielded type is the annotation when typed, else the first // concrete yield's type (optional when any yield is `none`). build_value_loop :: proc( ctx: ^Build_Ctx, body: ^[dynamic]hir.Stmt_Id, loop_id: ast.Stmt_Id, expected: types.Type, span: source.Span, ) -> (value: hir.Expr_Id, value_type: types.Type) { checker := ctx.checker loop_stmt := checker.ast_module.statements[loop_id] n := len(loop_stmt.body) last_is_fallthrough := n > 0 && checker.ast_module.statements[loop_stmt.body[n - 1]].kind == .Yield && !symbol.is_valid(checker.ast_module.statements[loop_stmt.body[n - 1]].label) if !symbol.is_valid(loop_stmt.label) { id := source.add(checker.diagnostics, span, "a value loop must label its body (e.g. 'blk:') so a 'yield :blk' can exit it") ctx.problematic^ = true return invalid_hir_expr(checker, span, id), types.INVALID } if !last_is_fallthrough { id := source.add(checker.diagnostics, span, "a value loop's body must end with a 'yield' for when the loop completes") ctx.problematic^ = true return invalid_hir_expr(checker, span, id), types.INVALID } fall_stmt := checker.ast_module.statements[loop_stmt.body[n - 1]] result_optional := loop_yields_none(checker, loop_stmt.body) slot := hir.INVALID_LOCAL slot_type := types.INVALID if is_runtime_type(checker, expected) { slot_type = expected slot = new_value_slot(ctx, slot_type) result_optional = types.is_optional(slot_type, &checker.module.types) } else if result_optional { // Untyped loop that also yields `none`: pre-type the element from the first // concrete yield (regardless of source order) so a `none` built before any // concrete yield still resolves the result to `?T`. elem := value_loop_element_type(ctx, loop_stmt) if is_runtime_type(checker, elem) { slot_type = types.optional(&checker.module.types, elem) slot = new_value_slot(ctx, slot_type) } } append(ctx.yield_targets, Yield_Target{ label = loop_stmt.label, slot = slot, slot_type = slot_type, result_optional = result_optional, defer_floor = len(ctx.defers^), }) // Build the loop with the fall-through peeled off, reusing the normal For/While arm. // The peeled body is a fresh copy so destroy_module won't double-free the original. peeled := loop_stmt peeled_body := make([]ast.Stmt_Id, n - 1, checker.ast_module.allocator) copy(peeled_body, loop_stmt.body[:n - 1]) peeled.body = peeled_body peeled_id := ast.stmt_id(len(checker.ast_module.statements)) append(&checker.ast_module.statements, peeled) loop_block := build_block(ctx, []ast.Stmt_Id{peeled_id}) target := pop(ctx.yield_targets) // The fall-through value initializes the slot before the loop (loop captures are // out of scope here), so the loop completing leaves it as the result. fall_value := build_expr(checker, fall_stmt.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, target.slot_type, ctx.pkg, ctx.file) fall_value = resolve_loop_slot(ctx, &target, fall_value, checker.module.exprs[fall_value].type if fall_value != hir.INVALID_EXPR else types.INVALID, fall_stmt.span) if target.slot == hir.INVALID_LOCAL || fall_value == hir.INVALID_EXPR { for s in loop_block { append(body, s) } delete(loop_block, checker.allocator) id := source.add(checker.diagnostics, span, "could not determine the value loop's yield type; annotate the binding") ctx.problematic^ = true return invalid_hir_expr(checker, span, id), types.INVALID } append(body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ kind = .Declaration, span = span, local = target.slot, expr = fall_value, diagnostic = source.INVALID_DIAGNOSTIC, }) for s in loop_block { append(body, s) } delete(loop_block, checker.allocator) value = slot_read(checker, target.slot, target.slot_type, span) return value, target.slot_type } // Reports whether every control-flow path through `stmts` terminates (returns or traps), // so the end of the block is unreachable. A `.Return` or `.Trap` terminates outright; an // `.If` terminates only when it has an `else` and both arms terminate. A literal // `while true` cannot fall through because the language has no `break` statement. // Recursion into the branch slices handles nested ifs and `else if` chains. all_paths_return :: proc(module: ^hir.Module, stmts: []hir.Stmt_Id) -> bool { for id in stmts { statement := module.statements[id] #partial switch statement.kind { case .Return, .Trap: return true case .If: if statement.else_body != nil && all_paths_return(module, statement.then_body) && all_paths_return(module, statement.else_body) { return true } case .While: // A literal `while true` makes the end of the block unreachable — // unless its body can `break` out of this loop. if statement.expr != hir.INVALID_EXPR && int(statement.expr) < len(module.exprs) { condition := module.exprs[statement.expr] if condition.kind == .Bool && condition.integer != 0 && !loop_body_breaks(module, statement.then_body) { return true } } } } return false } // Reports whether `stmts` contains a `break` that targets the enclosing loop: // a `.Break` at this level or inside `if`/`else` branches counts, but a `break` // inside a nested `.While`/`.For` targets that inner loop, so we do not descend. loop_body_breaks :: proc(module: ^hir.Module, stmts: []hir.Stmt_Id) -> bool { for id in stmts { statement := module.statements[id] #partial switch statement.kind { case .Break: return true case .If: if loop_body_breaks(module, statement.then_body) || loop_body_breaks(module, statement.else_body) { return true } } } return false } // Like `all_paths_return`, but also treats `break`/`continue` as terminating the // block. Used only to decide whether `build_block` may skip the fall-through defer // flush (a block that always exits early would otherwise emit unreachable copies). all_paths_exit :: proc(module: ^hir.Module, stmts: []hir.Stmt_Id) -> bool { for id in stmts { statement := module.statements[id] #partial switch statement.kind { case .Return, .Trap, .Break, .Continue: return true case .If: if statement.else_body != nil && all_paths_exit(module, statement.then_body) && all_paths_exit(module, statement.else_body) { return true } case .While: if statement.expr != hir.INVALID_EXPR && int(statement.expr) < len(module.exprs) { condition := module.exprs[statement.expr] if condition.kind == .Bool && condition.integer != 0 && !loop_body_breaks(module, statement.then_body) { return true } } } } return false } build_function :: proc(checker: ^Checker, id: Spec_Id) { spec := checker.specs[id] function := checker.ast_module.functions[spec.template] previous_comptime := checker.current_comptime_values checker.current_comptime_values = spec.comptime_values defer checker.current_comptime_values = previous_comptime signature_diagnostic := source.INVALID_DIAGNOSTIC if !types.is_void(spec.result) && !is_runtime_type(checker, spec.result) { checker.specs[id].result = types.I64 spec.result = types.I64 signature_diagnostic = source.addf( checker.diagnostics, function.span, "could not resolve a concrete result type for '%s'", symbol_text(checker, function.name), ) } for arg in spec.args { if !is_runtime_type(checker, arg) { signature_diagnostic = source.addf( checker.diagnostics, function.span, "could not resolve a concrete parameter type for '%s'", symbol_text(checker, function.name), ) break } } assert(spec.hir_id == hir.function_id(len(checker.module.functions))) locals: [dynamic]Build_Local locals.allocator = checker.allocator hir_locals: [dynamic]hir.Local hir_locals.allocator = checker.allocator local_spans: [dynamic]source.Span local_spans.allocator = checker.allocator local_used: [dynamic]bool local_used.allocator = checker.allocator local_warnable: [dynamic]bool local_warnable.allocator = checker.allocator params: [dynamic]hir.Local_Id params.allocator = checker.allocator body: [dynamic]hir.Stmt_Id body.allocator = checker.allocator global_reads: [dynamic]hir.Global_Id global_reads.allocator = checker.allocator calls: [dynamic]hir.Function_Id calls.allocator = checker.allocator demanded: [dynamic]Spec_Id demanded.allocator = checker.allocator local_types, _ := infer_spec_locals_and_result(checker, id, &demanded) defer { delete(local_types, checker.allocator) delete(demanded) } runtime_index := 0 for param in function.params { if param.comptime_value { continue } param_type := types.INVALID if runtime_index < len(spec.args) { param_type = spec.args[runtime_index] } local_id := append_tracked_local( &hir_locals, &local_spans, &local_used, &local_warnable, hir.Local{name = param.name, type = param_type, parameter = true}, param.span, ) append(&locals, Build_Local{name = param.name, type = param_type, id = local_id}) append(¶ms, local_id) runtime_index += 1 } problematic := signature_diagnostic != source.INVALID_DIAGNOSTIC || checker.template_diagnostics[spec.template] != source.INVALID_DIAGNOSTIC if !function.has_body { assert(spec.hir_id == hir.function_id(len(checker.module.functions))) append( &checker.module.functions, hir.Function { name = function.name, link_name = make_link_name(checker, id), calling_convention = .C if function.c_abi else .Brolang, implementation = .Declaration, linkage = .External if function.c_abi else .Internal, is_main = function.pkg == 0 && function.name == checker.main_symbol, variadic = function.variadic, params = params[:], result = spec.result, locals = hir_locals[:], body = body[:], direct_global_reads = global_reads, calls = calls[:], problematic = problematic, diagnostic = checker.template_diagnostics[spec.template], }, ) delete(locals) delete(local_spans) delete(local_used) delete(local_warnable) return } if signature_diagnostic != source.INVALID_DIAGNOSTIC { append(&body, hir.stmt_id(len(checker.module.statements))) append( &checker.module.statements, hir.Stmt { kind = .Trap, span = function.span, expr = hir.INVALID_EXPR, local = hir.INVALID_LOCAL, diagnostic = signature_diagnostic, }, ) } defers: [dynamic][]hir.Stmt_Id defers.allocator = checker.allocator loop_defer_starts: [dynamic]int loop_defer_starts.allocator = checker.allocator loop_labels: [dynamic]symbol.Id loop_labels.allocator = checker.allocator loop_is_loop: [dynamic]bool loop_is_loop.allocator = checker.allocator yield_targets: [dynamic]Yield_Target yield_targets.allocator = checker.allocator ctx := Build_Ctx{ checker = checker, pkg = function.pkg, file = function.file, result = spec.result, local_types = local_types, locals = &locals, hir_locals = &hir_locals, local_spans = &local_spans, local_used = &local_used, local_warnable = &local_warnable, global_reads = &global_reads, calls = &calls, problematic = &problematic, defers = &defers, loop_defer_starts = &loop_defer_starts, loop_labels = &loop_labels, loop_is_loop = &loop_is_loop, yield_targets = &yield_targets, } previous_result := checker.current_result previous_ctx := checker.current_build_ctx checker.current_result = spec.result checker.current_build_ctx = &ctx block := build_block(&ctx, function.body) checker.current_result = previous_result checker.current_build_ctx = previous_ctx returns := all_paths_return(&checker.module, block) for block_stmt in block { append(&body, block_stmt) } delete(block, checker.allocator) if !types.is_void(spec.result) && !returns { id := source.addf( checker.diagnostics, function.span, "function '%s' does not return a value", symbol_text(checker, function.name), ) append(&body, hir.stmt_id(len(checker.module.statements))) append( &checker.module.statements, hir.Stmt{kind = .Trap, span = function.span, expr = hir.INVALID_EXPR, local = hir.INVALID_LOCAL, diagnostic = id}, ) problematic = true } record_unused_locals(checker, hir_locals[:], local_spans[:], local_used[:], local_warnable[:]) assert(spec.hir_id == hir.function_id(len(checker.module.functions))) append( &checker.module.functions, hir.Function { name = function.name, link_name = make_link_name(checker, id), calling_convention = .C if function.c_abi || (function.pkg == 0 && function.name == checker.main_symbol) else .Brolang, implementation = .Definition, linkage = .External if function.c_abi || (function.pkg == 0 && function.name == checker.main_symbol) else .Internal, is_main = function.pkg == 0 && function.name == checker.main_symbol, variadic = function.variadic, params = params[:], result = spec.result, locals = hir_locals[:], body = body[:], direct_global_reads = global_reads, calls = calls[:], problematic = problematic, diagnostic = source.INVALID_DIAGNOSTIC, }, ) for entry in defers { delete(entry, checker.allocator) } delete(defers) delete(loop_defer_starts) delete(loop_labels) delete(loop_is_loop) delete(yield_targets) delete(locals) delete(local_spans) delete(local_used) delete(local_warnable) } expr_problematic :: proc(checker: ^Checker, expr_id: hir.Expr_Id) -> bool { module := &checker.module stack := checker.hir_expr_stack clear_dynamic_array(&stack) defer { clear_dynamic_array(&stack) checker.hir_expr_stack = stack } append(&stack, expr_id) for len(stack) > 0 { id := pop(&stack) if id == hir.INVALID_EXPR || int(id) >= len(module.exprs) { return true } expr := module.exprs[id] if expr.kind == .Invalid { return true } if expr.left != hir.INVALID_EXPR { append(&stack, expr.left) } if expr.right != hir.INVALID_EXPR { append(&stack, expr.right) } append(&stack, ..expr.args) } return false } static_integer_value :: proc(module: ^hir.Module, expr_id: hir.Expr_Id) -> (i64, bool) { current := expr_id for current != hir.INVALID_EXPR && int(current) < len(module.exprs) { expr := module.exprs[current] if expr.kind == .Integer { return expr.integer, true } if expr.kind != .Retype { break } current = expr.left } return 0, false } build_globals :: proc(checker: ^Checker) { for global, global_index in checker.ast_module.globals { if global.external { global_type := checker.global_types[global_index] writable := global.writable canonical := checker.external_global_canonical[global_index] if canonical != ast.INVALID_GLOBAL && int(canonical) < len(checker.ast_module.globals) { canonical_global := checker.ast_module.globals[canonical] writable = canonical_global.writable global_type = checker.global_types[canonical] } diagnostic := checker.external_global_diagnostics[global_index] if !is_runtime_type(checker, global_type) { if diagnostic == source.INVALID_DIAGNOSTIC { diagnostic = source.addf( checker.diagnostics, global.span, "could not resolve a concrete type for external global '%s'", symbol_text(checker, global.name), ) } global_type = types.I64 } _ = hir.global_id(len(checker.module.globals)) append(&checker.module.globals, hir.Global{ name=global.name, link_name=strings.clone(global.link_name, checker.allocator), type=global_type, expr=hir.INVALID_EXPR, external=true, writable=writable, direct_problem=diagnostic != source.INVALID_DIAGNOSTIC, problematic=diagnostic != source.INVALID_DIAGNOSTIC, diagnostic=diagnostic, }) continue } dependencies: [dynamic]hir.Global_Id dependencies.allocator = checker.allocator calls: [dynamic]hir.Function_Id calls.allocator = checker.allocator declared := resolve_inferred_array(checker, type_from_syntax(checker, global.type, global.pkg, global.file), global.expr) if has_inferred_array_count(checker, declared) && is_runtime_type(checker, checker.global_types[global_index]) { declared = checker.global_types[global_index] } expected := types.INVALID if is_runtime_type(checker, declared) { expected = declared } else if constant := eval_integer_constant_in_context(checker, global.expr, global.pkg, global.file); constant.kind == .Value && fits_i64(constant.value) && is_runtime_type(checker, checker.global_types[global_index]) { // Open integer constant: build against its demanded/defaulted type. Gated // to the infer-side open-constant condition so out-of-range constants keep // their original "exceeds signed i64 range" diagnostic. expected = checker.global_types[global_index] } else if (is_float_constant_expr(checker, global.expr) || is_numeric_arithmetic_expr(checker, global.expr)) && is_runtime_type(checker, checker.global_types[global_index]) { expected = checker.global_types[global_index] } expr := build_expr(checker, global.expr, nil, &dependencies, &calls, expected, global.pkg, global.file) global_type := checker.global_types[global_index] if is_runtime_type(checker, declared) { expr = coerce_expr(checker, expr, declared, global.span) global_type = checker.module.exprs[expr].type } else if is_runtime_type(checker, checker.module.exprs[expr].type) { global_type = checker.module.exprs[expr].type } diagnostic := source.INVALID_DIAGNOSTIC if !global.immutable && is_undefined_expr(checker, global.expr) { diagnostic = source.add( checker.diagnostics, global.span, "'undefined' is only valid as a mutable local declaration initializer", ) if !is_runtime_type(checker, global_type) { global_type = types.I64 } expr = invalid_hir_expr(checker, global.span, diagnostic, global_type) } if diagnostic == source.INVALID_DIAGNOSTIC && !is_runtime_type(checker, global_type) { diagnostic = source.addf( checker.diagnostics, global.span, "could not resolve a concrete type for global '%s'", symbol_text(checker, global.name), ) global_type = types.I64 expr = invalid_hir_expr(checker, global.span, diagnostic, global_type) } if global.type == types.VOID { diagnostic = source.add( checker.diagnostics, global.span, "void is only valid as a function result type", ) expr = invalid_hir_expr(checker, global.span, diagnostic) } if !global.immutable { if !types.is_valid(global.type) { diagnostic = source.addf( checker.diagnostics, global.span, "mutable global '%s' requires a type annotation", symbol_text(checker, global.name), ) global_type = types.I64 expr = invalid_hir_expr(checker, global.span, diagnostic, global_type) } } static_value, is_static := static_integer_value(&checker.module, expr) is_static = is_static && diagnostic == source.INVALID_DIAGNOSTIC && global.immutable _ = hir.global_id(len(checker.module.globals)) append( &checker.module.globals, hir.Global { name = global.name, link_name = strings.clone(global.link_name, checker.allocator), type = global_type, expr = expr, static_value = static_value, is_static = is_static, external = false, writable = !global.immutable, dependencies = dependencies, calls = calls[:], direct_problem = expr_problematic(checker, expr), problematic = expr_problematic(checker, expr), diagnostic = diagnostic, }, ) } } propagate_problems :: proc(checker: ^Checker) { changed := true for changed { changed = false for &function in checker.module.functions { if function.problematic { continue } for call in function.calls { if call != hir.INVALID_FUNCTION && int(call) < len(checker.module.functions) && checker.module.functions[call].problematic { function.problematic = true changed = true break } } } for &global in checker.module.globals { if global.problematic { continue } for dependency in global.dependencies { if dependency != hir.INVALID_GLOBAL && int(dependency) < len(checker.module.globals) && checker.module.globals[dependency].problematic { global.problematic = true changed = true break } } if global.problematic { continue } for call in global.calls { if call != hir.INVALID_FUNCTION && int(call) < len(checker.module.functions) && checker.module.functions[call].problematic { global.problematic = true changed = true break } } } } } append_unique_global :: proc(values: ^[dynamic]hir.Global_Id, value: hir.Global_Id) -> bool { for existing in values^ { if existing == value { return false } } append(values, value) return true } propagate_global_reads :: proc(checker: ^Checker) { changed := true for changed { changed = false for &function in checker.module.functions { for call in function.calls { if call == hir.INVALID_FUNCTION || int(call) >= len(checker.module.functions) { continue } for global_id in checker.module.functions[call].direct_global_reads { if append_unique_global(&function.direct_global_reads, global_id) { changed = true } } } } } for &global in checker.module.globals { for call in global.calls { if call == hir.INVALID_FUNCTION || int(call) >= len(checker.module.functions) { continue } for dependency in checker.module.functions[call].direct_global_reads { _ = append_unique_global(&global.dependencies, dependency) } } } } Cycle_Frame :: struct { global: hir.Global_Id, next_dependency: int, } cycle_global_location :: proc(checker: ^Checker, global_id: hir.Global_Id) -> (name, path: string, line: int) { name = "" path = "" line = 1 index := int(global_id) if global_id == hir.INVALID_GLOBAL || index >= len(checker.module.globals) { return } name = symbol_text(checker, checker.module.globals[global_id].name) if index >= len(checker.ast_module.globals) { return } span := checker.ast_module.globals[global_id].span source_file := source.source_for_span(checker.diagnostics, span) if source_file == nil { return } path = source_file.path line, _ = source.line_and_column(source_file, span.start) return } detect_global_cycles_visit :: proc(checker: ^Checker, global_id: hir.Global_Id, states: []u8) { if states[global_id] == 2 { return } stack := checker.cycle_stack clear_dynamic_array(&stack) defer { clear_dynamic_array(&stack) checker.cycle_stack = stack } append(&stack, Cycle_Frame{global=global_id}) for len(stack) > 0 { frame_index := len(stack)-1 frame := &stack[frame_index] if states[frame.global] == 0 { states[frame.global] = 1 } dependencies := checker.module.globals[frame.global].dependencies if frame.next_dependency >= len(dependencies) { states[frame.global] = 2 if checker.module.globals[frame.global].problematic && frame_index > 0 { checker.module.globals[stack[frame_index-1].global].problematic = true } _ = pop(&stack) continue } dependency := dependencies[frame.next_dependency] frame.next_dependency += 1 if dependency == hir.INVALID_GLOBAL || int(dependency) >= len(states) { continue } if states[dependency] == 1 { start_name, start_path, start_line := cycle_global_location(checker, dependency) end_name, end_path, end_line := cycle_global_location(checker, frame.global) id := source.addf( checker.diagnostics, checker.ast_module.globals[dependency].span, "global initialization cycle from '%s' at %s:%d to '%s' at %s:%d", start_name, start_path, start_line, end_name, end_path, end_line, ) checker.module.globals[dependency].diagnostic = id checker.module.globals[dependency].problematic = true checker.module.globals[frame.global].problematic = true continue } if states[dependency] == 2 { if checker.module.globals[dependency].problematic { checker.module.globals[frame.global].problematic = true } continue } append(&stack, Cycle_Frame{global=dependency}) } } synthesize_trap_main :: proc(checker: ^Checker) { id := source.add(checker.diagnostics, source.Span{}, "missing or unusable main function") statement_id := hir.stmt_id(len(checker.module.statements)) _ = hir.function_id(len(checker.module.functions)) append( &checker.module.statements, hir.Stmt{kind = .Trap, span = source.Span{}, expr = hir.INVALID_EXPR, local = hir.INVALID_LOCAL, diagnostic = id}, ) body := make([]hir.Stmt_Id, 1, checker.allocator) body[0] = statement_id append( &checker.module.functions, hir.Function { name = checker.main_symbol, link_name = fmt.aprintf("main", allocator = checker.allocator), calling_convention = .C, implementation = .Definition, linkage = .External, is_main = true, result = types.VOID, body = body, problematic = true, diagnostic = id, }, ) } replace_main_with_trap :: proc(checker: ^Checker, diagnostic: source.Diagnostic_Id) { for &function in checker.module.functions { if !function.is_main { continue } delete(function.params, checker.allocator) delete(function.body, checker.allocator) function.params = nil function.result = types.VOID function.calling_convention = .C function.implementation = .Definition function.linkage = .External function.problematic = true function.diagnostic = diagnostic statement_id := hir.stmt_id(len(checker.module.statements)) append( &checker.module.statements, hir.Stmt { kind = .Trap, span = source.Span{}, expr = hir.INVALID_EXPR, local = hir.INVALID_LOCAL, diagnostic = diagnostic, }, ) function.body = make([]hir.Stmt_Id, 1, checker.allocator) function.body[0] = statement_id return } synthesize_trap_main(checker) } check :: proc( ast_module: ^ast.Module, diagnostics: ^source.Diagnostics, symbols: ^symbol.Table, selected := target.DEFAULT, allocator := context.allocator, ) -> hir.Module { checker := Checker { ast_module = ast_module, diagnostics = diagnostics, symbols = symbols, module = hir.init_module(selected, allocator), main_symbol = symbol.intern(symbols, "main"), sink_symbol = symbol.intern(symbols, "_"), type_symbol = symbol.intern(symbols, "type"), target = selected, allocator = allocator, } checker.specs.allocator = allocator types.destroy_store(&checker.module.types) checker.module.types = types.clone_store(&ast_module.type_store, allocator) checker.module.types.selected = selected for value in ast_module.strings { append(&checker.module.strings, strings.clone(value, allocator)) } checker.constant_stack.allocator = allocator checker.ast_expr_stack.allocator = allocator checker.hir_expr_stack.allocator = allocator checker.infer_stack.allocator = allocator checker.build_stack.allocator = allocator checker.cycle_stack.allocator = allocator checker.anon_globals.allocator = allocator checker.type_factories.allocator = allocator checker.generated_types.allocator = allocator checker.type_factory_origins.allocator = allocator build_symbol_indexes(&checker) checker.global_types = make([]types.Type, len(ast_module.globals), allocator) checker.global_demands = make([]types.Type, len(ast_module.globals), allocator) checker.global_open_const = make([]bool, len(ast_module.globals), allocator) checker.global_open_float = make([]bool, len(ast_module.globals), allocator) checker.global_const_value = make([]i128, len(ast_module.globals), allocator) checker.external_global_canonical = make([]ast.Global_Id, len(ast_module.globals), allocator) checker.external_global_diagnostics = make([]source.Diagnostic_Id, len(ast_module.globals), allocator) for &canonical in checker.external_global_canonical { canonical = ast.INVALID_GLOBAL } for &diagnostic in checker.external_global_diagnostics { diagnostic = source.INVALID_DIAGNOSTIC } checker.constants = make([]Constant, len(ast_module.exprs), allocator) checker.template_diagnostics = make([]source.Diagnostic_Id, len(ast_module.functions), allocator) for &diagnostic in checker.template_diagnostics { diagnostic = source.INVALID_DIAGNOSTIC } defer { for spec in checker.specs { delete(spec.args, allocator) delete(spec.comptime_values, allocator) } delete(checker.specs) delete(checker.function_index, allocator) delete(checker.global_index, allocator) delete(checker.import_index, allocator) delete(checker.global_types, allocator) delete(checker.global_demands, allocator) delete(checker.global_open_const, allocator) delete(checker.global_open_float, allocator) delete(checker.global_const_value, allocator) delete(checker.external_global_canonical, allocator) delete(checker.external_global_diagnostics, allocator) delete(checker.constants, allocator) delete(checker.template_diagnostics, allocator) delete(checker.constant_stack) delete(checker.ast_expr_stack) delete(checker.hir_expr_stack) delete(checker.infer_stack) delete(checker.build_stack) delete(checker.cycle_stack) for entry in checker.type_factories { delete(entry.values, allocator) } for entry in checker.generated_types { delete(entry.values, allocator) } for origin in checker.type_factory_origins { delete(origin.values, allocator) } delete(checker.type_factories) delete(checker.generated_types) delete(checker.type_factory_origins) } for function, index in ast_module.functions { if function.generated { continue } for previous in ast_module.functions[:index] { if !previous.generated && previous.pkg == function.pkg && previous.name == function.name && declarations_conflict(function.file, function.file_hidden, previous.file, previous.file_hidden) { source.addf(diagnostics, function.span, "duplicate function '%s'", symbol_text(&checker, function.name)) } } for global in ast_module.globals { if global.pkg == function.pkg && global.name == function.name && declarations_conflict(function.file, function.file_hidden, global.file, global.file_hidden) { source.addf(diagnostics, function.span, "package declaration '%s' conflicts with a global", symbol_text(&checker, function.name)) } } if type_declaration_conflicts(&checker, function.pkg, function.name, function.file, function.file_hidden) { source.addf(diagnostics, function.span, "function '%s' shadows visible type", symbol_text(&checker, function.name)) } } for global, index in ast_module.globals { for previous in ast_module.globals[:index] { if previous.pkg == global.pkg && previous.name == global.name && declarations_conflict(global.file, global.file_hidden, previous.file, previous.file_hidden) { source.addf(diagnostics, global.span, "duplicate global '%s'", symbol_text(&checker, global.name)) } } if type_declaration_conflicts(&checker, global.pkg, global.name, global.file, global.file_hidden) { source.addf(diagnostics, global.span, "global '%s' shadows visible type", symbol_text(&checker, global.name)) } } validate_type_nodes(&checker) validate_declarations(&checker) infer_all(&checker) validate_external_globals(&checker) prune_specs(&checker) build_globals(&checker) for index := 0; index < len(checker.specs); index += 1 { build_function(&checker, spec_id(index)) } // Flush anonymous globals synthesized for `&`. Appended only now // (after every ast global was built at its identity-mapped index) so their ids, // pre-assigned as len(ast.globals)+stage_index, land exactly. for anon in checker.anon_globals { append(&checker.module.globals, anon) } propagate_global_reads(&checker) main_template := find_template(&checker, checker.main_symbol, 0) main_declarations := 0 for function in ast_module.functions { if function.pkg == 0 && function.name == checker.main_symbol { main_declarations += 1 } } if main_declarations == 0 { synthesize_trap_main(&checker) } else { template := ast_module.functions[main_template] if main_declarations != 1 || !template.has_body || len(template.params) != 0 || !(template.result == types.VOID || template.result == types.I32 || template.result == types.INT) { id := checker.template_diagnostics[main_template] if id == source.INVALID_DIAGNOSTIC { id = source.add( diagnostics, template.span, "main must be unique, have a body, take no parameters, and return void, i32, or int", ) } replace_main_with_trap(&checker, id) } } propagate_problems(&checker) states := make([]u8, len(checker.module.globals), allocator) for index in 0 ..< len(checker.module.globals) { detect_global_cycles_visit(&checker, hir.global_id(index), states) } delete(states, allocator) propagate_problems(&checker) for import_item in ast_module.imports { if import_item.valid && !import_item.used { source.addf_warning(diagnostics, import_item.span, "unused import '%s'", symbol_text(&checker, import_item.alias)) } } return checker.module }