package checker import "../ast" import "../hir" import "../source" import "../symbol" import "../types" import "base:intrinsics" import "core:fmt" import "core:mem" import "core:slice" import "core:strings" Spec :: struct { template: int, args: []types.Type, result: types.Type, hir_id: int, } Infer_Local :: struct { name: symbol.Id, type: types.Type, } Build_Local :: struct { name: symbol.Id, type: types.Type, mutable: bool, id: int, } Constant_Kind :: enum { Unknown, Not_Constant, Value, Overflow, } Constant :: struct { kind: Constant_Kind, value: i128, } Symbol_Index_Entry :: struct { scope: int, name: symbol.Id, id: int, } Checker :: struct { ast_module: ^ast.Module, diagnostics: ^source.Diagnostics, symbols: ^symbol.Table, module: hir.Module, specs: [dynamic]Spec, function_index: []Symbol_Index_Entry, global_index: []Symbol_Index_Entry, import_index: []Symbol_Index_Entry, global_types: []types.Type, constants: []Constant, template_diagnostics: []int, main_symbol: symbol.Id, sink_symbol: symbol.Id, allocator: mem.Allocator, } symbol_text :: proc(checker: ^Checker, id: symbol.Id) -> string { return symbol.resolve(checker.symbols, id) } eval_constant :: proc(checker: ^Checker, expr_id: int) -> Constant { if expr_id < 0 || expr_id >= len(checker.ast_module.exprs) { return Constant{kind = .Not_Constant} } if checker.constants[expr_id].kind != .Unknown { return checker.constants[expr_id] } expr := checker.ast_module.exprs[expr_id] result: Constant switch expr.kind { case .Integer: result = Constant{kind = .Value, value = i128(expr.integer)} case .Add: left := eval_constant(checker, expr.left) right := eval_constant(checker, expr.right) if left.kind == .Overflow || right.kind == .Overflow { result = Constant{kind = .Overflow} } else if left.kind != .Value || right.kind != .Value { result = Constant{kind = .Not_Constant} } else { value, overflow := intrinsics.overflow_add(left.value, right.value) if overflow { result = Constant{kind = .Overflow} } else { result = Constant{kind = .Value, value = value} } } case .Invalid, .Name, .Call: result = Constant{kind = .Not_Constant} } checker.constants[expr_id] = result return result } fits_signed_type :: proc(value: i128, target: types.Type) -> bool { if !types.is_signed(target) { return false } limit := i128(1) << u32(target.bits - 1) return value >= -limit && value < limit } fits_i64 :: proc(value: i128) -> bool { return fits_signed_type(value, types.I64) } type_from_syntax :: proc(value: ast.Type_Syntax) -> types.Type { switch value { case .Int: return types.INT case .I8: return types.I8 case .I16: return types.I16 case .I32: return types.I32 case .I64: return types.I64 case .Void: return types.VOID case .Invalid: return types.INVALID } return types.INVALID } symbol_index_less :: proc(left, right: Symbol_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_symbol :: proc(index: []Symbol_Index_Entry, scope: int, name: symbol.Id) -> int { 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 -1 } build_symbol_indexes :: proc(checker: ^Checker) { checker.function_index = make([]Symbol_Index_Entry, len(checker.ast_module.functions), checker.allocator) for function, id in checker.ast_module.functions { checker.function_index[id] = Symbol_Index_Entry{scope=function.pkg, name=function.name, id=id} } slice.sort_by(checker.function_index, symbol_index_less) checker.global_index = make([]Symbol_Index_Entry, len(checker.ast_module.globals), checker.allocator) for global, id in checker.ast_module.globals { checker.global_index[id] = Symbol_Index_Entry{scope=global.pkg, name=global.name, id=id} } slice.sort_by(checker.global_index, symbol_index_less) checker.import_index = make([]Symbol_Index_Entry, len(checker.ast_module.imports), checker.allocator) for import_item, id in checker.ast_module.imports { checker.import_index[id] = Symbol_Index_Entry{scope=import_item.file, name=import_item.alias, id=id} } slice.sort_by(checker.import_index, symbol_index_less) } find_template :: proc(checker: ^Checker, name: symbol.Id, pkg := 0) -> int { return find_symbol(checker.function_index, pkg, name) } find_global :: proc(checker: ^Checker, name: symbol.Id, pkg := 0) -> int { return find_symbol(checker.global_index, pkg, name) } find_import :: proc(checker: ^Checker, file: int, alias: symbol.Id, mark_used := false) -> int { id := find_symbol(checker.import_index, file, alias) if id >= 0 && mark_used { checker.ast_module.imports[id].used = true } return id } expr_package :: proc(checker: ^Checker, expr: ast.Expr, pkg, file: int, mark_used := false) -> (int, bool) { if !symbol.is_valid(expr.qualifier) { return pkg, true } import_id := find_import(checker, file, expr.qualifier, mark_used) if import_id < 0 { return -1, false } import_item := checker.ast_module.imports[import_id] if import_item.target < 0 || 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: int) -> int { if find_import(checker, file, expr.qualifier) < 0 { 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: int) -> int { if find_template(checker, expr.name, target_pkg) >= 0 { 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: int) -> int { if find_global(checker, expr.name, target_pkg) >= 0 { 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)) } 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, file: int) { if expr_id < 0 || expr_id >= len(checker.ast_module.exprs) { return } expr := checker.ast_module.exprs[expr_id] switch expr.kind { case .Name: if symbol.is_valid(expr.qualifier) { _ = find_import(checker, file, expr.qualifier, true) } case .Call: if symbol.is_valid(expr.qualifier) { _ = find_import(checker, file, expr.qualifier, true) } for arg in expr.args { mark_expr_imports_used(checker, arg, file) } case .Add: mark_expr_imports_used(checker, expr.left, file) mark_expr_imports_used(checker, expr.right, file) case .Invalid, .Integer: } } validate_declarations :: proc(checker: ^Checker) { for function, function_id in checker.ast_module.functions { locals: [dynamic]symbol.Id locals.allocator = checker.allocator for param in function.params { if param.type == .Void { source.add( checker.diagnostics, param.span, "void is only valid as a function result type", ) } if contains_name(locals[:], param.name) { source.addf( checker.diagnostics, param.span, "duplicate parameter '%s'", symbol_text(checker, param.name), ) } append(&locals, param.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.has_body && function.c_abi { for param in function.params { if type_from_syntax(param.type).kind != .Concrete { 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 result.kind != .Concrete && result.kind != .Void { 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", ) } } for statement_id in function.body { statement := checker.ast_module.statements[statement_id] switch statement.kind { case .Declaration, .Assignment, .Return, .Expression: mark_expr_imports_used(checker, statement.expr, function.file) case .Invalid: } } 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 } checker.template_diagnostics[function_id] = source.addf( checker.diagnostics, function.span, "duplicate foreign symbol '%s'", symbol_text(checker, function.name), ) break } } } find_infer_local :: proc(locals: []Infer_Local, name: symbol.Id) -> types.Type { for index := len(locals) - 1; index >= 0; index -= 1 { if locals[index].name == name { return locals[index].type } } return types.INVALID } spec_signature_equal :: proc(spec: Spec, template: int, args: []types.Type) -> bool { if spec.template != template || len(spec.args) != len(args) { return false } for arg, index in args { if !types.equal(spec.args[index], arg) { return false } } return true } specialized_param_type :: proc(syntax: ast.Type_Syntax, actual: types.Type) -> types.Type { declared := type_from_syntax(syntax) if declared.kind == .Int_Constraint { return actual } return declared } can_specialize :: proc(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 !types.is_concrete_integer(specialized_param_type(param.type, actual)) { return false } } return true } ensure_spec :: proc(checker: ^Checker, template: int, actual_args: []types.Type) -> int { 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(param.type, actual)) } for spec, index in checker.specs { if spec_signature_equal(spec, template, signature[:]) { delete(signature) return index } } result := type_from_syntax(function.result) if function.pkg == 0 && function.name == checker.main_symbol && function.result == .Int { result = types.I32 } index := len(checker.specs) append( &checker.specs, Spec{template = template, args = signature[:], result = result, hir_id = -1}, ) return index } infer_expr :: proc(checker: ^Checker, expr_id: int, locals: []Infer_Local, pkg := 0, file := 0) -> types.Type { if expr_id < 0 || expr_id >= len(checker.ast_module.exprs) { return types.INVALID } constant := eval_constant(checker, expr_id) if constant.kind == .Overflow || (constant.kind == .Value && !fits_i64(constant.value)) { return types.I64 } if constant.kind == .Value { return types.smallest_signed_for_literal(i64(constant.value)) } expr := checker.ast_module.exprs[expr_id] switch expr.kind { case .Invalid: return types.INVALID case .Integer: return types.smallest_signed_for_literal(expr.integer) case .Name: if !symbol.is_valid(expr.qualifier) { local_type := find_infer_local(locals, expr.name) if types.is_valid(local_type) { return local_type } } target_pkg, available := expr_package(checker, expr, pkg, file) if !available { return types.INVALID } global := find_global(checker, expr.name, target_pkg) if global >= 0 { return checker.global_types[global] } return types.INVALID case .Add: left := infer_expr(checker, expr.left, locals, pkg, file) right := infer_expr(checker, expr.right, locals, pkg, file) return types.widest(left, right) case .Call: target_pkg, available := expr_package(checker, expr, pkg, file) if !available { return types.INVALID } template := find_template(checker, expr.name, target_pkg) if template < 0 { return types.INVALID } if checker.template_diagnostics[template] >= 0 { declared := type_from_syntax(checker.ast_module.functions[template].result) if declared.kind == .Concrete || declared.kind == .Void { return declared } return types.INVALID } args := make([]types.Type, len(expr.args), checker.allocator) for arg, index in expr.args { args[index] = infer_expr(checker, arg, locals, pkg, file) } function := checker.ast_module.functions[template] if !can_specialize(function, args) { delete(args, checker.allocator) declared := type_from_syntax(function.result) if function.pkg == 0 && function.name == checker.main_symbol && function.result == .Int { return types.I32 } if declared.kind == .Concrete || declared.kind == .Void { return declared } return types.INVALID } spec := ensure_spec(checker, template, args) delete(args, checker.allocator) return checker.specs[spec].result } return types.INVALID } infer_spec_result :: proc(checker: ^Checker, spec_id: int) -> types.Type { spec := checker.specs[spec_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 == .Int { declared = types.I32 } locals: [dynamic]Infer_Local locals.allocator = checker.allocator defer delete(locals) 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}) } result := types.INVALID for statement_id in function.body { statement := checker.ast_module.statements[statement_id] #partial switch statement.kind { case .Declaration: value_type := infer_expr(checker, statement.expr, locals[:], function.pkg, function.file) declared_local := type_from_syntax(statement.type) if declared_local.kind == .Concrete { value_type = declared_local } append(&locals, Infer_Local{name = statement.name, type = value_type}) case .Assignment, .Expression: _ = infer_expr(checker, statement.expr, locals[:], function.pkg, function.file) case .Return: if statement.expr >= 0 { returned := infer_expr(checker, statement.expr, locals[:], function.pkg, function.file) if !types.is_valid(result) { result = returned } else { result = types.widest(result, returned) } } } } if declared.kind == .Int_Constraint { return result } return declared } merge_inferred_type :: proc(current: ^types.Type, inferred: types.Type) -> bool { if !types.is_concrete_integer(inferred) { return false } if !types.is_concrete_integer(current^) { current^ = inferred return true } merged := types.widest(current^, inferred) if types.is_concrete_integer(merged) && !types.equal(current^, merged) { current^ = merged return true } return false } infer_all :: proc(checker: ^Checker) { for global, index in checker.ast_module.globals { declared := type_from_syntax(global.type) if declared.kind == .Concrete { checker.global_types[index] = declared } } main_template := find_template(checker, checker.main_symbol, 0) if main_template >= 0 { ensure_spec(checker, main_template, nil) } for { changed := false spec_count := len(checker.specs) for global, index in checker.ast_module.globals { if type_from_syntax(global.type).kind == .Concrete { continue } inferred := infer_expr(checker, global.expr, nil, global.pkg, global.file) changed = merge_inferred_type(&checker.global_types[index], inferred) || changed } for spec_id := 0; spec_id < len(checker.specs); spec_id += 1 { inferred := infer_spec_result(checker, spec_id) changed = merge_inferred_type(&checker.specs[spec_id].result, inferred) || changed } if len(checker.specs) != spec_count { changed = true } if !changed { break } } } add_hir_expr :: proc(checker: ^Checker, expr: hir.Expr) -> int { id := len(checker.module.exprs) append(&checker.module.exprs, expr) return id } invalid_hir_expr :: proc( checker: ^Checker, span: source.Span, diagnostic: int, recovery_type := types.INVALID, ) -> int { return add_hir_expr( checker, hir.Expr { kind = .Invalid, span = span, type = recovery_type, target = -1, left = -1, right = -1, diagnostic = diagnostic, }, ) } add_unique :: proc(values: ^[dynamic]int, value: int) { 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: int, expected: types.Type, span: source.Span, ) -> int { if expr_id < 0 { return expr_id } actual := checker.module.exprs[expr_id].type if types.equal(actual, expected) { return expr_id } if types.can_widen(actual, expected) { return add_hir_expr( checker, hir.Expr { kind = .Widen, span = span, type = expected, left = expr_id, target = -1, right = -1, diagnostic = -1, }, ) } 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) } build_constant_expr :: proc( checker: ^Checker, expr: ast.Expr, constant: Constant, expected: types.Type, ) -> int { recovery_type := types.I64 if types.is_signed(expected) { recovery_type = expected } if constant.kind == .Overflow || !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) result_type := types.smallest_signed_for_literal(value) if types.is_signed(expected) { if !fits_signed_type(constant.value, expected) { 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 = -1, left = -1, right = -1, diagnostic = -1, }, ) } build_expr :: proc( checker: ^Checker, expr_id: int, locals: []Build_Local, global_reads: ^[dynamic]int, calls: ^[dynamic]int, expected := types.INVALID, pkg := 0, file := 0, ) -> int { if expr_id < 0 || expr_id >= len(checker.ast_module.exprs) { id := source.add(checker.diagnostics, source.Span{}, "missing expression") return invalid_hir_expr(checker, source.Span{}, id) } expr := checker.ast_module.exprs[expr_id] constant := eval_constant(checker, expr_id) if constant.kind == .Value || constant.kind == .Overflow { return build_constant_expr(checker, expr, constant, expected) } switch expr.kind { case .Invalid: return invalid_hir_expr(checker, expr.span, expr.diagnostic) case .Integer: unreachable() case .Name: if !symbol.is_valid(expr.qualifier) { if local, ok := find_build_local(locals, expr.name); ok { return add_hir_expr( checker, hir.Expr { kind = .Local, span = expr.span, type = local.type, target = local.id, left = -1, right = -1, diagnostic = -1, }, ) } } target_pkg, available := expr_package(checker, expr, pkg, file, true) if !available { id := add_package_resolution_diagnostic(checker, expr, file) return invalid_hir_expr(checker, expr.span, id) } global := find_global(checker, expr.name, target_pkg) if global >= 0 { add_unique(global_reads, global) return add_hir_expr( checker, hir.Expr { kind = .Global, span = expr.span, type = checker.global_types[global], target = global, left = -1, right = -1, diagnostic = -1, }, ) } id := add_name_resolution_diagnostic(checker, expr, target_pkg) return invalid_hir_expr(checker, expr.span, id) case .Add: left := build_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file) right := build_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 result := types.widest(left_type, right_type) if !types.is_signed(result) { id := source.add( checker.diagnostics, expr.span, "addition requires compatible signed integers", ) return invalid_hir_expr(checker, expr.span, id) } left = coerce_expr(checker, left, result, checker.module.exprs[left].span) right = coerce_expr(checker, right, result, checker.module.exprs[right].span) return add_hir_expr( checker, hir.Expr { kind = .Add, span = expr.span, type = result, left = left, right = right, target = -1, diagnostic = -1, }, ) case .Call: target_pkg, available := expr_package(checker, expr, pkg, file, true) if !available { id := add_package_resolution_diagnostic(checker, expr, file) return invalid_hir_expr(checker, expr.span, id) } template := find_template(checker, expr.name, target_pkg) if template < 0 { id := add_call_resolution_diagnostic(checker, expr, target_pkg) return invalid_hir_expr(checker, expr.span, id) } if checker.template_diagnostics[template] >= 0 { return invalid_hir_expr(checker, expr.span, checker.template_diagnostics[template]) } if len(expr.args) != len(checker.ast_module.functions[template].params) { id := source.addf( checker.diagnostics, expr.span, "function '%s' expects %d arguments, got %d", symbol_text(checker, expr.name), len(checker.ast_module.functions[template].params), len(expr.args), ) return invalid_hir_expr(checker, expr.span, id) } built_args := make([]int, len(expr.args), checker.allocator) arg_types := make([]types.Type, len(expr.args), checker.allocator) for arg, index in expr.args { arg_expected := type_from_syntax(checker.ast_module.functions[template].params[index].type) if arg_expected.kind != .Concrete { arg_expected = types.INVALID } built_args[index] = build_expr( checker, arg, locals, global_reads, calls, arg_expected, pkg, file, ) arg_types[index] = checker.module.exprs[built_args[index]].type } spec := ensure_spec(checker, template, arg_types) delete(arg_types, checker.allocator) for _, index in built_args { built_args[index] = coerce_expr( checker, built_args[index], checker.specs[spec].args[index], checker.module.exprs[built_args[index]].span, ) } add_unique(calls, spec) result := checker.specs[spec].result if !types.is_valid(result) { id := source.addf( checker.diagnostics, expr.span, "could not resolve result type for specialization of '%s'", symbol_text(checker, expr.name), ) delete(built_args, checker.allocator) return invalid_hir_expr(checker, expr.span, id) } return add_hir_expr( checker, hir.Expr { kind = .Call, span = expr.span, type = result, target = spec, left = -1, right = -1, args = built_args, diagnostic = -1, }, ) } return invalid_hir_expr( checker, expr.span, source.add(checker.diagnostics, expr.span, "invalid expression"), ) } make_link_name :: proc(checker: ^Checker, spec_id: int) -> string { spec := checker.specs[spec_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 { 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, "__") strings.write_string(&builder, types.name(arg)) } return fmt.aprintf("%s", strings.to_string(builder), allocator = checker.allocator) } build_function :: proc(checker: ^Checker, spec_id: int) { if checker.specs[spec_id].hir_id >= 0 { return } spec := checker.specs[spec_id] function := checker.ast_module.functions[spec.template] signature_diagnostic := -1 if spec.result.kind != .Void && !types.is_concrete_integer(spec.result) { checker.specs[spec_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 !types.is_concrete_integer(arg) { signature_diagnostic = source.addf( checker.diagnostics, function.span, "could not resolve a concrete parameter type for '%s'", symbol_text(checker, function.name), ) break } } hir_id := len(checker.module.functions) checker.specs[spec_id].hir_id = hir_id locals: [dynamic]Build_Local locals.allocator = checker.allocator hir_locals: [dynamic]hir.Local hir_locals.allocator = checker.allocator params: [dynamic]int params.allocator = checker.allocator body: [dynamic]int body.allocator = checker.allocator global_reads: [dynamic]int global_reads.allocator = checker.allocator calls: [dynamic]int calls.allocator = checker.allocator for param, index in function.params { 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 >= 0 || checker.template_diagnostics[spec.template] >= 0 if !function.has_body { append( &checker.module.functions, hir.Function { name = function.name, link_name = make_link_name(checker, spec_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, 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 } has_return := false if signature_diagnostic >= 0 { append(&body, len(checker.module.statements)) append( &checker.module.statements, hir.Stmt { kind = .Trap, span = function.span, expr = -1, local = -1, diagnostic = signature_diagnostic, }, ) } for statement_id in function.body { statement := checker.ast_module.statements[statement_id] switch statement.kind { case .Declaration: declared := type_from_syntax(statement.type) expected := types.INVALID if declared.kind == .Concrete { expected = declared } value := build_expr( checker, statement.expr, locals[:], &global_reads, &calls, expected, function.pkg, function.file, ) value_type := checker.module.exprs[value].type if declared.kind == .Concrete { value = coerce_expr(checker, value, declared, statement.span) value_type = checker.module.exprs[value].type } else if declared.kind == .Void { 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(locals[:], statement.name); found { id := source.addf( checker.diagnostics, statement.span, "duplicate local '%s'", symbol_text(checker, statement.name), ) append(&body, len(checker.module.statements)) append( &checker.module.statements, hir.Stmt { kind = .Trap, span = statement.span, expr = -1, local = -1, diagnostic = id, }, ) problematic = true continue } local_id := len(hir_locals) append( &hir_locals, hir.Local { name = statement.name, type = value_type, mutable = !statement.immutable, }, ) append( &locals, Build_Local { name = statement.name, type = value_type, mutable = !statement.immutable, id = local_id, }, ) append(&body, len(checker.module.statements)) append( &checker.module.statements, hir.Stmt { kind = .Declaration, span = statement.span, local = local_id, expr = value, diagnostic = -1, }, ) problematic = problematic || checker.module.exprs[value].kind == .Invalid case .Assignment: if statement.name == checker.sink_symbol { value := build_expr(checker, statement.expr, locals[:], &global_reads, &calls, types.INVALID, function.pkg, function.file) if checker.module.exprs[value].type.kind == .Void { id := source.add( checker.diagnostics, statement.span, "cannot assign a void expression to '_'", ) append(&body, len(checker.module.statements)) append( &checker.module.statements, hir.Stmt { kind = .Trap, span = statement.span, expr = -1, local = -1, diagnostic = id, }, ) problematic = true } else { append(&body, len(checker.module.statements)) append( &checker.module.statements, hir.Stmt { kind = .Sink, span = statement.span, expr = value, local = -1, diagnostic = -1, }, ) } continue } local, found := find_build_local(locals[:], statement.name) if !found { id := source.addf( checker.diagnostics, statement.span, "cannot assign unresolved local '%s'", symbol_text(checker, statement.name), ) append(&body, len(checker.module.statements)) append( &checker.module.statements, hir.Stmt { kind = .Trap, span = statement.span, expr = -1, local = -1, diagnostic = id, }, ) 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, len(checker.module.statements)) append( &checker.module.statements, hir.Stmt { kind = .Trap, span = statement.span, expr = -1, local = -1, diagnostic = id, }, ) problematic = true continue } value := build_expr( checker, statement.expr, locals[:], &global_reads, &calls, local.type, function.pkg, function.file, ) value = coerce_expr(checker, value, local.type, statement.span) append(&body, len(checker.module.statements)) append( &checker.module.statements, hir.Stmt { kind = .Assignment, span = statement.span, expr = value, local = local.id, diagnostic = -1, }, ) problematic = problematic || checker.module.exprs[value].kind == .Invalid case .Return: has_return = true if statement.expr < 0 { if spec.result.kind != .Void { id := source.add( checker.diagnostics, statement.span, "'return _' is only valid in a void function", ) append(&body, len(checker.module.statements)) append( &checker.module.statements, hir.Stmt { kind = .Trap, span = statement.span, expr = -1, local = -1, diagnostic = id, }, ) problematic = true } else { append(&body, len(checker.module.statements)) append( &checker.module.statements, hir.Stmt { kind = .Return, span = statement.span, expr = -1, local = -1, diagnostic = -1, }, ) } continue } if spec.result.kind == .Void { id := source.add( checker.diagnostics, statement.span, "void function cannot return a value", ) append(&body, len(checker.module.statements)) append( &checker.module.statements, hir.Stmt { kind = .Trap, span = statement.span, expr = -1, local = -1, diagnostic = id, }, ) problematic = true continue } value := build_expr( checker, statement.expr, locals[:], &global_reads, &calls, spec.result, function.pkg, function.file, ) value = coerce_expr(checker, value, spec.result, statement.span) append(&body, len(checker.module.statements)) append( &checker.module.statements, hir.Stmt { kind = .Return, span = statement.span, expr = value, local = -1, diagnostic = -1, }, ) problematic = problematic || checker.module.exprs[value].kind == .Invalid case .Expression: value := build_expr(checker, statement.expr, locals[:], &global_reads, &calls, types.INVALID, function.pkg, function.file) if checker.module.exprs[value].type.kind != .Void { id := source.add( checker.diagnostics, statement.span, "non-void expression result must be consumed or assigned to '_'", ) append(&body, len(checker.module.statements)) append( &checker.module.statements, hir.Stmt { kind = .Trap, span = statement.span, expr = -1, local = -1, diagnostic = id, }, ) problematic = true } else { append(&body, len(checker.module.statements)) append( &checker.module.statements, hir.Stmt { kind = .Expression, span = statement.span, expr = value, local = -1, diagnostic = -1, }, ) } case .Invalid: append(&body, len(checker.module.statements)) append( &checker.module.statements, hir.Stmt { kind = .Trap, span = statement.span, expr = -1, local = -1, diagnostic = statement.diagnostic, }, ) problematic = true } } if spec.result.kind != .Void && !has_return { id := source.addf( checker.diagnostics, function.span, "function '%s' does not return a value", symbol_text(checker, function.name), ) append(&body, len(checker.module.statements)) append( &checker.module.statements, hir.Stmt{kind = .Trap, span = function.span, expr = -1, local = -1, diagnostic = id}, ) problematic = true } append( &checker.module.functions, hir.Function { name = function.name, link_name = make_link_name(checker, spec_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, params = params[:], result = spec.result, locals = hir_locals[:], body = body[:], direct_global_reads = global_reads[:], calls = calls[:], problematic = problematic, diagnostic = -1, }, ) delete(locals) } expr_problematic :: proc(module: ^hir.Module, expr_id: int) -> bool { if expr_id < 0 || expr_id >= len(module.exprs) { return true } expr := module.exprs[expr_id] if expr.kind == .Invalid { return true } if expr.left >= 0 && expr_problematic(module, expr.left) { return true } if expr.right >= 0 && expr_problematic(module, expr.right) { return true } for arg in expr.args { if expr_problematic(module, arg) { return true } } return false } build_globals :: proc(checker: ^Checker) { for global, global_id in checker.ast_module.globals { dependencies: [dynamic]int dependencies.allocator = checker.allocator calls: [dynamic]int calls.allocator = checker.allocator declared := type_from_syntax(global.type) expected := types.INVALID if declared.kind == .Concrete { expected = declared } expr := build_expr(checker, global.expr, nil, &dependencies, &calls, expected, global.pkg, global.file) global_type := checker.global_types[global_id] if declared.kind == .Concrete { expr = coerce_expr(checker, expr, declared, global.span) global_type = checker.module.exprs[expr].type } else if types.is_concrete_integer(checker.module.exprs[expr].type) { global_type = checker.module.exprs[expr].type } diagnostic := -1 if !types.is_concrete_integer(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 == .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) } is_static := checker.module.exprs[expr].kind == .Integer && diagnostic < 0 static_value: i64 if is_static { static_value = checker.module.exprs[expr].integer } append( &checker.module.globals, hir.Global { name = global.name, type = global_type, expr = expr, static_value = static_value, is_static = is_static, dependencies = dependencies[:], calls = calls[:], direct_problem = expr_problematic(&checker.module, expr), problematic = expr_problematic(&checker.module, 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 >= 0 && call < len(checker.specs) { hir_id := checker.specs[call].hir_id if hir_id >= 0 && checker.module.functions[hir_id].problematic { function.problematic = true changed = true break } } } } for &global in checker.module.globals { if global.problematic { continue } for dependency in global.dependencies { if dependency >= 0 && 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 >= 0 && call < len(checker.specs) { hir_id := checker.specs[call].hir_id if hir_id >= 0 && checker.module.functions[hir_id].problematic { global.problematic = true changed = true break } } } } } } resolve_call_targets :: proc(checker: ^Checker) { for &expr in checker.module.exprs { if expr.kind == .Call && expr.target >= 0 && expr.target < len(checker.specs) { expr.target = checker.specs[expr.target].hir_id } } } append_unique_slice :: proc(values: ^[]int, value: int, allocator: mem.Allocator) -> bool { for existing in values^ { if existing == value { return false } } replacement := make([]int, len(values^) + 1, allocator) copy(replacement, values^) replacement[len(values^)] = value delete(values^, allocator) values^ = replacement 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 < 0 || call >= len(checker.specs) { continue } callee := checker.specs[call].hir_id if callee < 0 || callee >= len(checker.module.functions) { continue } for global_id in checker.module.functions[callee].direct_global_reads { if append_unique_slice( &function.direct_global_reads, global_id, checker.allocator, ) { changed = true } } } } } for &global in checker.module.globals { for call in global.calls { if call < 0 || call >= len(checker.specs) { continue } function_id := checker.specs[call].hir_id if function_id < 0 || function_id >= len(checker.module.functions) { continue } for dependency in checker.module.functions[function_id].direct_global_reads { _ = append_unique_slice(&global.dependencies, dependency, checker.allocator) } } } } detect_global_cycles_visit :: proc(checker: ^Checker, global_id: int, states: []u8) { if states[global_id] == 2 { return } if states[global_id] == 1 { id := source.addf( checker.diagnostics, checker.ast_module.globals[global_id].span, "global initialization cycle involving '%s'", symbol_text(checker, checker.module.globals[global_id].name), ) checker.module.globals[global_id].diagnostic = id checker.module.globals[global_id].problematic = true return } states[global_id] = 1 for dependency in checker.module.globals[global_id].dependencies { if dependency >= 0 && dependency < len(states) { detect_global_cycles_visit(checker, dependency, states) if checker.module.globals[dependency].problematic { checker.module.globals[global_id].problematic = true } } } states[global_id] = 2 } synthesize_trap_main :: proc(checker: ^Checker) { id := source.add(checker.diagnostics, source.Span{}, "missing or unusable main function") statement_id := len(checker.module.statements) append( &checker.module.statements, hir.Stmt{kind = .Trap, span = source.Span{}, expr = -1, local = -1, diagnostic = id}, ) body := make([]int, 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: int) { 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 := len(checker.module.statements) append( &checker.module.statements, hir.Stmt { kind = .Trap, span = source.Span{}, expr = -1, local = -1, diagnostic = diagnostic, }, ) function.body = make([]int, 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, allocator := context.allocator, ) -> hir.Module { checker := Checker { ast_module = ast_module, diagnostics = diagnostics, symbols = symbols, module = hir.init_module(allocator), main_symbol = symbol.intern(symbols, "main"), sink_symbol = symbol.intern(symbols, "_"), allocator = allocator, } checker.specs.allocator = allocator build_symbol_indexes(&checker) checker.global_types = make([]types.Type, len(ast_module.globals), allocator) checker.constants = make([]Constant, len(ast_module.exprs), allocator) checker.template_diagnostics = make([]int, len(ast_module.functions), allocator) for &diagnostic in checker.template_diagnostics { diagnostic = -1 } 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.constants, allocator) delete(checker.template_diagnostics, allocator) } 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_declarations(&checker) infer_all(&checker) build_globals(&checker) for spec_id := 0; spec_id < len(checker.specs); spec_id += 1 { build_function(&checker, spec_id) } resolve_call_targets(&checker) 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 == .Void || template.result == .I32 || template.result == .Int) { id := checker.template_diagnostics[main_template] if id < 0 { 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 global_id in 0 ..< len(checker.module.globals) { detect_global_cycles_visit(&checker, global_id, 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 }