package checker import "../ast" import "../hir" import "../source" import "../symbol" import "../target" import "../types" import "base:intrinsics" 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, 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 marks a local whose initializer is a compile-time integer with no // concrete annotation: like an open-constant global, it is sign-agnostic until a // backward demand from a use picks its family/width (see merge_local_demand). open_const: 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. 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, global_reads: ^[dynamic]hir.Global_Id, calls: ^[dynamic]hir.Function_Id, problematic: ^bool, } Constant_Kind :: enum { Unknown, Not_Constant, Value, Overflow, Div_By_Zero, } Constant :: struct { kind: Constant_Kind, value: i128, } Function_Index_Entry :: struct { scope: ast.Package_Id, name: symbol.Id, id: ast.Function_Id, } Global_Index_Entry :: struct { scope: ast.Package_Id, name: symbol.Id, id: ast.Global_Id, } Import_Index_Entry :: struct { scope: ast.File_Id, name: symbol.Id, id: ast.Import_Id, } 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_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, main_symbol: symbol.Id, sink_symbol: symbol.Id, target: target.Target, allocator: mem.Allocator, } symbol_text :: proc(checker: ^Checker, id: symbol.Id) -> string { return symbol.resolve(checker.symbols, id) } Constant_Frame :: struct { expr: ast.Expr_Id, stage: u8, } eval_constant :: proc(checker: ^Checker, expr_id: ast.Expr_Id) -> Constant { if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) { return Constant{kind = .Not_Constant} } stack := checker.constant_stack clear_dynamic_array(&stack) defer { clear_dynamic_array(&stack) checker.constant_stack = stack } append(&stack, Constant_Frame{expr=expr_id}) for len(stack) > 0 { frame_index := len(stack)-1 frame := stack[frame_index] if checker.constants[frame.expr].kind != .Unknown { _ = pop(&stack) continue } expr := checker.ast_module.exprs[frame.expr] if expr.kind != .Add && expr.kind != .Sub && expr.kind != .Mul && expr.kind != .Div && expr.kind != .Negate { result := Constant{kind = .Not_Constant} if expr.kind == .Integer { result = Constant{kind = .Value, value = i128(expr.integer)} } checker.constants[frame.expr] = result _ = pop(&stack) continue } if frame.stage == 0 { stack[frame_index].stage = 1 if expr.left != ast.INVALID_EXPR && int(expr.left) < len(checker.ast_module.exprs) && checker.constants[expr.left].kind == .Unknown { append(&stack, Constant_Frame{expr=expr.left}) } continue } if frame.stage == 1 && expr.kind == .Negate { operand := Constant{kind = .Not_Constant} if expr.left != ast.INVALID_EXPR && int(expr.left) < len(checker.constants) { operand = checker.constants[expr.left] } result := Constant{kind = .Not_Constant} if operand.kind == .Div_By_Zero { result = Constant{kind = .Div_By_Zero} } else if operand.kind == .Overflow { result = Constant{kind = .Overflow} } else if operand.kind == .Value { value, overflow := intrinsics.overflow_sub(i128(0), operand.value) result = Constant{kind = .Overflow} if overflow else Constant{kind = .Value, value = value} } checker.constants[frame.expr] = result _ = pop(&stack) continue } if frame.stage == 1 { stack[frame_index].stage = 2 if expr.right != ast.INVALID_EXPR && int(expr.right) < len(checker.ast_module.exprs) && checker.constants[expr.right].kind == .Unknown { append(&stack, Constant_Frame{expr=expr.right}) } continue } left := Constant{kind = .Not_Constant} right := Constant{kind = .Not_Constant} if expr.left != ast.INVALID_EXPR && int(expr.left) < len(checker.constants) { left = checker.constants[expr.left] } if expr.right != ast.INVALID_EXPR && int(expr.right) < len(checker.constants) { right = checker.constants[expr.right] } result := Constant{kind = .Not_Constant} if left.kind == .Div_By_Zero || right.kind == .Div_By_Zero { result = Constant{kind = .Div_By_Zero} } else if left.kind == .Overflow || right.kind == .Overflow { result = Constant{kind = .Overflow} } else if left.kind == .Value && right.kind == .Value { value: i128 overflow: bool div_by_zero: bool #partial switch expr.kind { case .Sub: value, overflow = intrinsics.overflow_sub(left.value, right.value) case .Mul: value, overflow = intrinsics.overflow_mul(left.value, right.value) case .Div: if right.value == 0 { div_by_zero = true } else { value = left.value / right.value } case: value, overflow = intrinsics.overflow_add(left.value, right.value) } switch { case div_by_zero: result = Constant{kind = .Div_By_Zero} case overflow: result = Constant{kind = .Overflow} case: result = Constant{kind = .Value, value = value} } } checker.constants[frame.expr] = result _ = pop(&stack) } return checker.constants[expr_id] } 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(value: ast.Type_Syntax) -> types.Type { return value } 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 } 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))) } 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) -> 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 } } if low < len(index) && index[low].scope == scope && index[low].name == name { return index[low].id } return ast.INVALID_FUNCTION } find_global_symbol :: proc(index: []Global_Index_Entry, scope: ast.Package_Id, name: symbol.Id) -> 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 } } if low < len(index) && index[low].scope == scope && index[low].name == name { return index[low].id } return ast.INVALID_GLOBAL } 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) { checker.function_index = make([]Function_Index_Entry, len(checker.ast_module.functions), checker.allocator) for function, id in checker.ast_module.functions { checker.function_index[id] = Function_Index_Entry{scope=function.pkg, name=function.name, id=ast.function_id(id)} } 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, 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)) -> ast.Function_Id { return find_function_symbol(checker.function_index, pkg, name) } find_global :: proc(checker: ^Checker, name: symbol.Id, pkg := ast.Package_Id(0)) -> ast.Global_Id { return find_global_symbol(checker.global_index, pkg, name) } 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 } 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 } 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) -> source.Diagnostic_Id { if find_template(checker, expr.name, target_pkg) != 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) -> source.Diagnostic_Id { if find_global(checker, expr.name, target_pkg) != 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) } return source.INVALID_DIAGNOSTIC } function_signatures_equal :: proc(left, right: ast.Function) -> bool { if left.result != right.result || 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 { 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_arg_expected :: proc(function: ast.Function, index: int) -> types.Type { if index < 0 || index >= len(function.params) { return types.INVALID } declared := type_from_syntax(function.params[index].type) // 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 } 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.c_abi { return nil, types.INVALID, false } result = type_from_syntax(function.result) 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(param.type) 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, ) -> (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, true, function.variadic) pointer_type := types.pointer(&checker.module.types, function_type, false, true) spec := INVALID_SPEC if demanded == nil { spec = ensure_spec(checker, template, params) } else { spec = find_spec(checker, template, params) mark_spec_demanded(checker, spec, demanded) } return pointer_type, spec, spec != INVALID_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, .Keyed: append(&stack, expr.left) 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, .Name, .Enum_Literal: } } } 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: mark_expr_imports_used(checker, statement.expr, file) if statement.target != ast.INVALID_EXPR { mark_expr_imports_used(checker, statement.target, 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 .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 } locals: [dynamic]symbol.Id locals.allocator = checker.allocator for param in function.params { if diagnostic := add_unsupported_type_diagnostic(checker, param.span, type_from_syntax(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), ) } append(&locals, param.name) if types.contains_c_struct_by_value(type_from_syntax(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 diagnostic := add_unsupported_type_diagnostic(checker, function.span, type_from_syntax(function.result)); diagnostic != source.INVALID_DIAGNOSTIC { checker.template_diagnostics[function_id] = diagnostic } if types.contains_c_struct_by_value(type_from_syntax(function.result), &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 !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(param.type) 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(function.result) 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", ) } for field in types.fields_for(&checker.module.types, id) { 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", ) } } } if item.kind == .Function { if !item.c_abi { source.add(checker.diagnostics, source.Span{}, "only c_func function pointer types are supported") } 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{}, "function 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{}, "function pointer results must be concrete C signature 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) -> Spec_Id { function := checker.ast_module.functions[template] for spec, index in checker.specs { if spec.template != template || len(spec.args) != len(function.params) { continue } matches := true for param, param_index in function.params { actual := types.INVALID if param_index < len(actual_args) { actual = actual_args[param_index] } if !types.equal(spec.args[param_index], specialized_param_type(checker, param.type, actual)) { matches = false break } } 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) -> types.Type { declared := type_from_syntax(syntax) 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) -> bool { for param, index in function.params { actual := types.INVALID if index < len(actual_args) { actual = actual_args[index] } if !is_runtime_type(checker, specialized_param_type(checker, param.type, actual)) { return false } } return true } ensure_spec :: proc(checker: ^Checker, template: ast.Function_Id, actual_args: []types.Type) -> Spec_Id { if existing := find_spec(checker, template, actual_args); existing != INVALID_SPEC { return existing } function := checker.ast_module.functions[template] signature: [dynamic]types.Type signature.allocator = checker.allocator for param, index in function.params { actual := types.INVALID if index < len(actual_args) { actual = actual_args[index] } append(&signature, specialized_param_type(checker, param.type, actual)) } result := type_from_syntax(function.result) if function.pkg == 0 && function.name == checker.main_symbol && function.result == types.INT { result = types.I32 } index := spec_id(len(checker.specs)) append( &checker.specs, Spec{template = template, args = 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, stage: u8, left: types.Type, arg_index: 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, ) -> 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) 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, ) -> types.Type { store := &checker.module.types #partial switch expr.kind { 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: return types.INVALID 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 { 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 .Struct_Literal: for keyed in expr.args { _ = infer_nested_expr(checker, checker.ast_module.exprs[keyed].left, locals, pkg, file, demanded, local_types) } target_pkg, available := expr_package(checker, expr, pkg, file) value := types.find_named(store, u32(target_pkg), u32(expr.name)) 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, ) -> 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, 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 .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, .Struct_Literal, .Keyed, .Enum_Literal, .Bool, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range: last = infer_compound_expr(checker, expr, locals, pkg, file, demanded, local_types) _ = 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) { 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) 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) 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 } target_pkg, available := expr_package(checker, expr, pkg, file) template := ast.INVALID_FUNCTION if available { template = find_template(checker, expr.name, target_pkg) } 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) 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)) 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 := type_from_syntax(checker.ast_module.functions[template].result) last = declared if is_runtime_type(checker, declared) || types.is_void(declared) else types.INVALID _ = pop(&stack) continue } stack[frame_index].template = template stack[frame_index].args = make([]types.Type, len(expr.args), checker.allocator) stack[frame_index].stage = 3 if len(expr.args) > 0 { append(&stack, Infer_Frame{expr=expr.args[0], 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 { if expr.kind == .Add && types.is_many_pointer(frame.left, &checker.module.types) && types.is_concrete_integer(last) { last = frame.left } else { last = types.widest(frame.left, last) } _ = pop(&stack) continue } if frame.stage == 3 { 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 := checker.ast_module.functions[frame.template] // A bare-name argument passed to a concrete (non-constraint) parameter pushes // that parameter type back onto the argument's slot, so an open constant adopts // it (e.g. `take_u16(a)` resolves `a` to u16). Constraint params have no single // type to demand; the callee's result flowing back is milestone 14.5. for arg_index in 0..= 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: declared_local := resolve_inferred_array(checker, type_from_syntax(statement.type), statement.expr) value_type := types.INVALID if !is_undefined_expr(checker, statement.expr) { value_type = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types) } if is_runtime_type(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 const_val := i128(0) if !is_runtime_type(checker, declared_local) && !is_undefined_expr(checker, statement.expr) { constant := eval_constant(checker, statement.expr) if constant.kind == .Value && fits_i64(constant.value) { open = true const_val = constant.value } } local := Infer_Local{ name=statement.name, type=value_type, declared=declared_local, statement=statement_id, mutable=!statement.immutable, open_const=open, 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). record_demand(checker, statement.expr, value_type, locals^[:], local_types, pkg, file) case .Assignment: value_type := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types) if statement.target != ast.INVALID_EXPR { _ = infer_expr(checker, statement.target, locals^[:], pkg, file, demanded, local_types) 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) } } } 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) } } case .Expression: _ = 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) if is_runtime_type(checker, result_hint) { 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(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) } } 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] declared := type_from_syntax(function.result) 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) for param, index in function.params { param_type := types.INVALID if index < len(spec.args) { param_type = spec.args[index] } append(&locals, Infer_Local{name=param.name, type=param_type, declared=param_type, statement=ast.INVALID_STMT}) } 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 } // root_demand_target returns the global that an initializer pushes a backward type // demand onto: when the initializer's root expression is a bare name referencing a // global (e.g. `Z i32 :: Y`). Returns INVALID_GLOBAL for any other shape — demands // deliberately do not flow through arithmetic, calls, or other operators (that is L3). root_demand_target :: proc(checker: ^Checker, expr_id: ast.Expr_Id, pkg: ast.Package_Id, file: ast.File_Id) -> ast.Global_Id { if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) { return ast.INVALID_GLOBAL } expr := checker.ast_module.exprs[expr_id] if expr.kind != .Name { return ast.INVALID_GLOBAL } target_pkg, available := expr_package(checker, expr, pkg, file) if !available { return ast.INVALID_GLOBAL } return find_global(checker, expr.name, target_pkg) } // merge_open_const_demand records a concrete integer demand onto an open-constant // global's slot. The constant is sign-agnostic until used, so it may adopt any // integer family/width whose range holds its value (first demand wins; later demands // may only widen within the chosen family). Non-integer or non-fitting demands are // ignored, leaving the constant to default and the genuine mismatch to surface at the // use's boundary coercion. merge_open_const_demand :: proc(checker: ^Checker, slot: ^types.Type, demand: types.Type, value: i128) -> bool { if !types.is_concrete_integer(demand) || !fits_integer_type(value, demand, checker.target) { 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 } changed: bool if checker.global_open_const[index] { changed = merge_open_const_demand(checker, &checker.global_demands[index], demand, 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 integer demand onto an open-constant local. // Like an open-constant global it adopts any integer family/width whose range holds its // value (gated by its constraint family if it has one); the first demand replaces the // literal's signed default, 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 || !types.is_concrete_integer(demand) { return false } if types.is_constraint(local.declared) && !types.constraint_accepts(local.declared, demand, &checker.module.types) { return false } if !fits_integer_type(local.const_value, demand, checker.target) { 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 } // record_demand pushes a concrete type demand onto the slot of a bare-name expression // (a typed declaration's initializer, a call argument, a return value). When the name // resolves to an open-constant local or global, that slot adopts the demand; any other // shape is ignored — demands flow only through bare names, never through arithmetic or // across a call's result (the latter is 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, ) { if !is_runtime_type(checker, demand) || expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) { return } expr := checker.ast_module.exprs[expr_id] if expr.kind != .Name { return } if !symbol.is_valid(expr.qualifier) { if index, ok := find_infer_local_index(locals, expr.name); ok { _ = merge_local_demand(checker, &locals[index], demand, local_types) return } } target_pkg, available := expr_package(checker, expr, pkg, file) if !available { return } global := find_global(checker, expr.name, target_pkg) if global != ast.INVALID_GLOBAL { _ = merge_global_demand(checker, global, demand) } } infer_all :: proc(checker: ^Checker) { // An "open constant" global has no concrete declared type and a compile-time // integer initializer. Its slot stays sign-agnostic so a backward demand from any // reachable use can pick its family/width; absent a demand it defaults to the // smallest signed type (legacy behaviour). Demands accumulate in global_demands so // the default never blocks a later cross-family (e.g. unsigned) demand. for global, index in checker.ast_module.globals { declared := type_from_syntax(global.type) if is_runtime_type(checker, declared) { checker.global_types[index] = declared continue } if global.external { continue } constant := eval_constant(checker, global.expr) if constant.kind == .Value && fits_i64(constant.value) { checker.global_open_const[index] = true checker.global_const_value[index] = constant.value } } main_template := find_template(checker, checker.main_symbol, 0) if main_template != ast.INVALID_FUNCTION { ensure_spec(checker, main_template, nil) } for { changed := false checker.global_demands_dirty = false spec_count := len(checker.specs) // Backward demands: a global whose initializer's root is a bare name referencing // another global pushes its own (declared or already-resolved) type onto that // referent. Open constants adopt any fitting family; other referents widen only. for global, index in checker.ast_module.globals { if global.external { continue } demand := checker.global_types[index] if !is_runtime_type(checker, demand) { continue } target := root_demand_target(checker, global.expr, global.pkg, global.file) if target != ast.INVALID_GLOBAL { merge_global_demand(checker, target, demand) } } // 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 } inferred := infer_expr(checker, global.expr, nil, global.pkg, global.file) if is_runtime_type(checker, type_from_syntax(global.type)) { 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] { resolved := types.smallest_signed_for_literal(i64(checker.global_const_value[index])) if !types.equal(checker.global_types[index], resolved) { checker.global_types[index] = resolved changed = true } } 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 { 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) 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 } 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.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_weaken_pointer(actual, child, &checker.module.types) || types.can_weaken_slice(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) { 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", types.name(actual), types.name(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, 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 } 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)) 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)) 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 C callback; expected a concrete c_func", 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, true, function.variadic) pointer_type := types.pointer(&checker.module.types, function_type, false, true) 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 } 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_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 .Address: 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) 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 || item.kind == .Pointer { id := source.add(checker.diagnostics, expr.span, "slicing requires an array, slice, or pointer-to-array") 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) } } 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) } 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 .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 .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] 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_const.kind == .Value && left_const.kind != .Value { 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_const.kind == .Value && right_const.kind != .Value { 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: target_pkg, available := expr_package(checker, expr, pkg, file, true) struct_type := types.find_named(store, u32(target_pkg), u32(expr.name)) 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) } 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 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 { if values[0] == hir.INVALID_EXPR { 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) } for value, index in initialized { if value { active_field = i64(index) break } } } 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, .Struct_Literal, .Keyed, .Bool, .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 .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 = add_hir_expr(checker, hir.Expr{ kind=.Local, span=expr.span, type=local.type, target=hir.local_ref(local.id), left = hir.INVALID_EXPR, right = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC, }) } } else if local, ok := find_build_local(locals, expr.qualifier); ok { base := add_hir_expr(checker, hir.Expr{ kind=.Local, span=expr.span, type=local.type, target=hir.local_ref(local.id), left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) 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 { 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); global != ast.INVALID_GLOBAL { last = build_global_reference(checker, global, expr.span, global_reads) } else { template := find_template(checker, expr.name, target_pkg) if template != ast.INVALID_FUNCTION && checker.ast_module.functions[template].c_abi { 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) } 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 } 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) 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 = add_hir_expr(checker, hir.Expr{ kind=.Local, span=expr.span, type=local.type, target=hir.local_ref(local.id), left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) } else { non_callable = true } } } if callee == hir.INVALID_EXPR && !non_callable { if global := find_global(checker, expr.name, target_pkg); 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)) 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) 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) } } 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 !valid_call_arity(function, len(expr.args)) { message := "function '%s' expects at least %d arguments, got %d" if function.variadic else "function '%s' expects %d arguments, got %d" 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].built_args = make([]hir.Expr_Id, len(expr.args), checker.allocator) stack[frame_index].arg_types = make([]types.Type, len(expr.args), checker.allocator) stack[frame_index].stage = 3 if len(expr.args) > 0 { arg_expected := call_arg_expected(function, 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 == 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 stack[frame_index].arg_types[frame.arg_index] = checker.module.exprs[last].type stack[frame_index].arg_index += 1 if frame.arg_index+1 < len(expr.args) { next := frame.arg_index+1 arg_expected := call_arg_expected(checker.ast_module.functions[frame.template], 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 } } arg_violation := source.INVALID_DIAGNOSTIC { params := checker.ast_module.functions[frame.template].params for index in 0..= len(stack[frame_index].arg_types) { break } declared := type_from_syntax(params[index].type) 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 } } } spec := find_spec(checker, frame.template, stack[frame_index].arg_types) delete(stack[frame_index].arg_types, checker.allocator) stack[frame_index].arg_types = nil if arg_violation != source.INVALID_DIAGNOSTIC { delete(stack[frame_index].built_args, checker.allocator) stack[frame_index].built_args = nil last = invalid_hir_expr(checker, expr.span, arg_violation) _ = 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 } fixed_count := len(checker.ast_module.functions[frame.template].params) for index in 0.. 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.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) 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)) } } return fmt.aprintf("%s", strings.to_string(builder), allocator = checker.allocator) } build_block :: proc( ctx: ^Build_Ctx, statements: []ast.Stmt_Id, duplicate_scope_start := -1, ) -> []hir.Stmt_Id { checker := ctx.checker body: [dynamic]hir.Stmt_Id body.allocator = checker.allocator scope_start := len(ctx.locals^) 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: declared := resolve_inferred_array(checker, type_from_syntax(statement.type), statement.expr) // Adopt the type inference resolved for this local when the declaration has no // concrete annotation (a constraint, `undefined`, or an un-annotated open // integer constant): the slot may have absorbed a backward demand (e.g. `a :: 10` // built as u16 after `take_u16(a)`). Gated to compile-time integer constants so // strings/arrays/pointers keep their own initializer type. open_const_decl := !is_runtime_type(checker, declared) && !is_undefined_expr(checker, statement.expr) if open_const_decl { constant := eval_constant(checker, statement.expr) open_const_decl = constant.kind == .Value && fits_i64(constant.value) } 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) { 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) { 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 } local_id := hir.local_id(len(ctx.hir_locals^)) append(ctx.hir_locals, hir.Local{ name = statement.name, type = value_type, mutable = !statement.immutable, }) append(ctx.locals, Build_Local{ name = statement.name, type = value_type, mutable = !statement.immutable, id = local_id, }) 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 { 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 := 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 } local, found := find_build_local(ctx.locals^[:], statement.name) if !found { 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 } 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 := 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 statement.expr == ast.INVALID_EXPR { 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 { 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 := 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) 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, }) ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid 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 } local = hir.local_id(len(ctx.hir_locals^)) append(ctx.hir_locals, hir.Local{name=capture, type=child, mutable=false}) append(ctx.locals, Build_Local{name=capture, type=child, mutable=false, id=local}) } 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 } loop_body := build_block(ctx, statement.body) 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, 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^) item_local := hir.local_id(len(ctx.hir_locals^)) append(ctx.hir_locals, hir.Local{name=statement.name, type=capture_type, mutable=false}) append(ctx.locals, Build_Local{name=statement.name, type=capture_type, mutable=false, id=item_local}) 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 { index_local = hir.local_id(len(ctx.hir_locals^)) append(ctx.hir_locals, hir.Local{name=statement.index_name, type=types.USIZE, mutable=false}) append(ctx.locals, Build_Local{name=statement.index_name, type=types.USIZE, mutable=false, id=index_local}) } } loop_body := build_block(ctx, statement.body, capture_start) 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, 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 .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 } } resize(ctx.locals, scope_start) return body[:] } // 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: if statement.expr != hir.INVALID_EXPR && int(statement.expr) < len(module.exprs) { condition := module.exprs[statement.expr] if condition.kind == .Bool && condition.integer != 0 { return true } } } } return false } build_function :: proc(checker: ^Checker, id: Spec_Id) { spec := checker.specs[id] function := checker.ast_module.functions[spec.template] 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 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) } for param, index in function.params { local_id := hir.local_id(len(hir_locals)) param_type := types.INVALID if index < len(spec.args) { param_type = spec.args[index] } append(&hir_locals, hir.Local{name = param.name, type = param_type, parameter = true}) append(&locals, Build_Local{name = param.name, type = param_type, id = local_id}) append(¶ms, local_id) } 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) 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, }, ) } ctx := Build_Ctx{ checker = checker, pkg = function.pkg, file = function.file, result = spec.result, local_types = local_types, locals = &locals, hir_locals = &hir_locals, global_reads = &global_reads, calls = &calls, problematic = &problematic, } block := build_block(&ctx, function.body) 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 } 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, }, ) delete(locals) } 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(global.type), global.expr) expected := types.INVALID if is_runtime_type(checker, declared) { expected = declared } else if constant := eval_constant(checker, global.expr); constant.kind == .Value && fits_i64(constant.value) && is_runtime_type(checker, checker.global_types[global_index]) { // Open constant: build the initializer against the type inference resolved // for this slot, so it adopts its demanded/defaulted type (e.g. `A :: 10` // built as u16 when a use demanded u16). Gated to compile-time values fitting // i64 — exactly 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] } 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 !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 { diagnostic = source.add( checker.diagnostics, global.span, "mutable declarations are only valid inside functions", ) expr = invalid_hir_expr(checker, global.span, diagnostic) } static_value, is_static := static_integer_value(&checker.module, expr) is_static = is_static && diagnostic == source.INVALID_DIAGNOSTIC _ = 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 = false, 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, } 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 { id := source.addf( checker.diagnostics, checker.ast_module.globals[dependency].span, "global initialization cycle involving '%s'", symbol_text(checker, checker.module.globals[dependency].name), ) 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, "_"), 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 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_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(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_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 function, index in ast_module.functions { for previous in ast_module.functions[:index] { if previous.pkg == function.pkg && previous.name == function.name { 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 { source.addf(diagnostics, function.span, "package declaration '%s' conflicts with a global", 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 { source.addf(diagnostics, global.span, "duplicate global '%s'", 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)) } 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(diagnostics, import_item.span, "unused import '%s'", symbol_text(&checker, import_item.alias)) } } return checker.module }