Files
brolang/compiler/checker/checker.odin
T

5319 lines
189 KiB
Odin

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/open_float mark a local whose initializer is an unannotated numeric
// constant: like an open-constant global, it can adopt a backward demand from use.
open_const: bool,
open_float: bool,
const_value: i128,
demanded: bool,
}
Build_Local :: struct {
name: symbol.Id,
type: types.Type,
mutable: bool,
id: hir.Local_Id,
}
// Build_Ctx threads the per-function accumulators through build_block so that
// nested control-flow blocks (if/else) can be built recursively. `locals` is a
// scope stack: each block records its entry length and truncates back to it on
// exit, while `hir_locals` keeps every allocated slot for the function.
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,
// Number of enclosing loops being built. `break`/`continue` are only valid
// when this is > 0; bumped around loop-body builds in `build_block`.
loop_depth: int,
}
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_open_float: []bool,
global_const_value: []i128,
global_demands_dirty: bool,
external_global_canonical: []ast.Global_Id,
external_global_diagnostics: []source.Diagnostic_Id,
constants: []Constant,
template_diagnostics: []source.Diagnostic_Id,
constant_stack: [dynamic]Constant_Frame,
ast_expr_stack: [dynamic]ast.Expr_Id,
hir_expr_stack: [dynamic]hir.Expr_Id,
infer_stack: [dynamic]Infer_Frame,
build_stack: [dynamic]Build_Expr_Frame,
cycle_stack: [dynamic]Cycle_Frame,
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
}
is_float_constant_expr :: proc(checker: ^Checker, expr_id: ast.Expr_Id) -> bool {
if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) {
return false
}
expr := checker.ast_module.exprs[expr_id]
if expr.kind == .Float {
return true
}
return expr.kind == .Negate && is_float_constant_expr(checker, expr.left)
}
is_numeric_arithmetic_expr :: proc(checker: ^Checker, expr_id: ast.Expr_Id) -> bool {
if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) {
return false
}
#partial switch checker.ast_module.exprs[expr_id].kind {
case .Add, .Sub, .Mul, .Div, .Negate:
return true
}
return false
}
is_numeric_constant_expr :: proc(checker: ^Checker, expr_id: ast.Expr_Id) -> bool {
if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) {
return false
}
return eval_constant(checker, expr_id).kind == .Value || is_float_constant_expr(checker, expr_id)
}
is_numeric_demand :: proc(value: types.Type, selected := target.DEFAULT) -> bool {
return types.is_concrete_scalar(value) && !types.is_bool(value) ||
types.is_float(value, selected)
}
string_literal_type :: proc(checker: ^Checker, string_id: u64) -> types.Type {
length: u64
if string_id < u64(len(checker.ast_module.strings)) {
length = u64(len(checker.ast_module.strings[string_id]))
}
array := types.array(&checker.module.types, types.U8, length, false, true, 0)
return types.pointer(&checker.module.types, array, false, false)
}
container_pointer_type :: proc(store: ^types.Store, item: types.Node) -> types.Type {
return types.pointer(store, item.child, item.mutable, true, item.has_sentinel, item.sentinel)
}
resolve_inferred_array :: proc(checker: ^Checker, value: types.Type, expr_id: ast.Expr_Id) -> types.Type {
item, ok := types.node(&checker.module.types, value)
if !ok || item.kind != .Array || !item.inferred_count ||
expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) {
return value
}
expr := checker.ast_module.exprs[expr_id]
if expr.kind != .Array {
return value
}
return types.with_array_count(&checker.module.types, value, u64(len(expr.args)))
}
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 .Break, .Continue:
case .Invalid:
}
}
}
validate_external_globals :: proc(checker: ^Checker) {
for global, global_index in checker.ast_module.globals {
checker.external_global_canonical[global_index] = ast.global_id(global_index)
if !global.external {
continue
}
switch global.link_name {
case "main":
checker.external_global_diagnostics[global_index] = source.add(
checker.diagnostics,
global.span,
"external C variable 'main' conflicts with the program entry point",
)
case "write":
checker.external_global_diagnostics[global_index] = source.add(
checker.diagnostics,
global.span,
"external C variable 'write' conflicts with the compiler runtime",
)
}
for previous, previous_index in checker.ast_module.globals[:global_index] {
if !previous.external || previous.link_name != global.link_name {
continue
}
canonical := checker.external_global_canonical[previous_index]
if canonical == ast.INVALID_GLOBAL {
canonical = ast.global_id(previous_index)
}
canonical_index := int(canonical)
if canonical_index < 0 || canonical_index >= len(checker.ast_module.globals) {
canonical = ast.global_id(previous_index)
canonical_index = previous_index
}
checker.external_global_canonical[global_index] = canonical
canonical_global := checker.ast_module.globals[canonical_index]
canonical_type := checker.global_types[canonical_index]
if !types.equal(checker.global_types[global_index], canonical_type) ||
global.writable != canonical_global.writable {
checker.external_global_diagnostics[global_index] = source.addf(
checker.diagnostics,
global.span,
"conflicting external C variable declarations for '%s'",
global.link_name,
)
}
checker.global_types[global_index] = canonical_type
break
}
for function in checker.ast_module.functions {
if !function.c_abi || function.has_body || len(function.unsupported_reason) > 0 ||
symbol_text(checker, function.name) != global.link_name {
continue
}
if checker.external_global_diagnostics[global_index] == source.INVALID_DIAGNOSTIC {
checker.external_global_diagnostics[global_index] = source.addf(
checker.diagnostics,
global.span,
"external C variable '%s' conflicts with a C function declaration",
global.link_name,
)
}
break
}
}
}
validate_declarations :: proc(checker: ^Checker) {
for function, function_id in checker.ast_module.functions {
if len(function.unsupported_reason) > 0 {
continue
}
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 {
right := last
if expr.kind == .Add && types.is_many_pointer(frame.left, &checker.module.types) && types.is_concrete_integer(right) {
last = frame.left
} else if is_numeric_constant_expr(checker, expr.right) &&
is_numeric_demand(frame.left, checker.target) &&
expr_accepts_numeric_demand(checker, expr.right, frame.left, locals, pkg, file) {
last = frame.left
} else if is_numeric_constant_expr(checker, expr.left) &&
is_numeric_demand(right, checker.target) &&
expr_accepts_numeric_demand(checker, expr.left, right, locals, pkg, file) {
last = right
} else if is_numeric_demand(frame.left, checker.target) &&
!numeric_operand_is_open(checker, expr.left, locals, pkg, file) &&
expr_accepts_numeric_demand(checker, expr.right, frame.left, locals, pkg, file) {
// Propagate only from an authoritative (fixed-type) left operand. A left
// operand that is still a provisional open constant carries only its
// smallest-signed default, which must not poison the sibling's family;
// two provisional operands are resolved together by the backward demand
// from the declaration/use.
_ = record_demand(checker, expr.right, frame.left, locals, local_types, pkg, file)
last = frame.left
} else if is_numeric_demand(right, checker.target) &&
!numeric_operand_is_open(checker, expr.right, locals, pkg, file) &&
expr_accepts_numeric_demand(checker, expr.left, right, locals, pkg, file) {
_ = record_demand(checker, expr.left, right, locals, local_types, pkg, file)
last = right
} else {
last = types.widest(frame.left, right)
}
_ = pop(&stack)
continue
}
if frame.stage == 3 {
if frame.arg_index < len(expr.args) {
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(expr.args) {
record_demand(checker, expr.args[arg_index], call_arg_expected(function, arg_index), locals, local_types, pkg, file)
}
// Deferred defaulting leaves an undemanded open constant typeless; give such an
// argument its default so the call can still monomorphize (the default feeds only
// the spec arg vector, not a demand).
for arg_index in 0..<len(expr.args) {
if !is_runtime_type(checker, stack[frame_index].args[arg_index]) {
fallback := open_const_default_type(checker, expr.args[arg_index], locals, pkg, file)
if is_runtime_type(checker, fallback) {
stack[frame_index].args[arg_index] = fallback
}
}
}
if valid_call_arity(function, len(expr.args)) &&
can_specialize(checker, function, stack[frame_index].args) {
spec := INVALID_SPEC
if demanded == nil {
spec = ensure_spec(checker, frame.template, stack[frame_index].args)
} else {
spec = find_spec(checker, frame.template, stack[frame_index].args)
mark_spec_demanded(checker, spec, demanded)
}
if spec != INVALID_SPEC {
last = checker.specs[spec].result
} else {
declared := type_from_syntax(function.result)
last = declared if is_runtime_type(checker, declared) || types.is_void(declared) else types.INVALID
}
} else {
declared := type_from_syntax(function.result)
if function.pkg == 0 && function.name == checker.main_symbol && function.result == types.INT {
last = types.I32
} else {
last = declared if is_runtime_type(checker, declared) || types.is_void(declared) else types.INVALID
}
}
delete(stack[frame_index].args, checker.allocator)
stack[frame_index].args = nil
_ = pop(&stack)
}
if frame.stage == 6 {
if frame.arg_index < len(expr.args) {
stack[frame_index].args[frame.arg_index] = last
stack[frame_index].arg_index += 1
if frame.arg_index+1 < len(expr.args) {
append(&stack, Infer_Frame{expr=expr.args[frame.arg_index+1], template=ast.INVALID_FUNCTION})
continue
}
}
function_item, ok := types.node(&checker.module.types, frame.left)
if ok && function_item.kind == .Function && valid_callable_arity(function_item, len(expr.args)) {
last = function_item.child
} else {
last = types.INVALID
}
delete(stack[frame_index].args, checker.allocator)
stack[frame_index].args = nil
_ = pop(&stack)
}
if frame.stage == 7 {
last = frame.left
_ = pop(&stack)
}
}
return last
}
flatten_conditional_unwrap_operands :: proc(
module: ^ast.Module,
expr_id: ast.Expr_Id,
operands: ^[dynamic]ast.Expr_Id,
) {
if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(module.exprs) {
append(operands, expr_id)
return
}
expr := module.exprs[expr_id]
if expr.kind == .And {
flatten_conditional_unwrap_operands(module, expr.left, operands)
flatten_conditional_unwrap_operands(module, expr.right, operands)
return
}
append(operands, expr_id)
}
record_infer_local_type :: proc(local: Infer_Local, local_types: []types.Type) {
if local.statement != ast.INVALID_STMT && int(local.statement) < len(local_types) {
local_types[local.statement] = local.type
}
}
merge_infer_local_type :: proc(
checker: ^Checker,
local: ^Infer_Local,
inferred: types.Type,
local_types: []types.Type,
) -> bool {
if !is_runtime_type(checker, inferred) {
return false
}
if types.is_constraint(local.declared) {
if !types.constraint_accepts(local.declared, inferred, &checker.module.types) {
return false
}
if !is_runtime_type(checker, local.type) {
local.type = inferred
record_infer_local_type(local^, local_types)
return true
}
if types.equal(local.type, inferred) {
return false
}
merged := types.widest(local.type, inferred)
if types.constraint_accepts(local.declared, merged, &checker.module.types) {
local.type = merged
record_infer_local_type(local^, local_types)
return true
}
return false
}
if is_runtime_type(checker, local.declared) {
local.type = local.declared
record_infer_local_type(local^, local_types)
return false
}
if !is_runtime_type(checker, local.type) {
local.type = inferred
record_infer_local_type(local^, local_types)
return true
}
if types.equal(local.type, inferred) {
return false
}
merged := types.widest(local.type, inferred)
if types.is_concrete_scalar(merged) {
local.type = merged
record_infer_local_type(local^, local_types)
return true
}
return false
}
infer_statements :: proc(
checker: ^Checker,
statements: []ast.Stmt_Id,
locals: ^[dynamic]Infer_Local,
local_types: []types.Type,
pkg: ast.Package_Id,
file: ast.File_Id,
demanded: ^[dynamic]Spec_Id,
result: ^types.Type,
result_hint := types.INVALID,
) {
scope_start := len(locals^)
for statement_id in statements {
statement := checker.ast_module.statements[statement_id]
#partial switch statement.kind {
case .Declaration:
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
open_float := 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
} else if is_float_constant_expr(checker, statement.expr) {
open_float = true
}
}
local := Infer_Local{
name=statement.name,
type=value_type,
declared=declared_local,
statement=statement_id,
mutable=!statement.immutable,
open_const=open,
open_float=open_float,
const_value=const_val,
}
append(locals, local)
record_infer_local_type(local, local_types)
// A typed/constraint declaration initialized by a bare name pushes its resolved
// type backward onto that name (mirrors the `Y int :: X; Z i32 :: Y` global chain).
// Skip this for a constraint/untyped declaration whose initializer is arithmetic:
// value_type is then only a provisional default (e.g. `x int = a - b` resolving to
// i16 before the operands' real uses are seen) and would poison the open-constant
// operands' family. The operands resolve from their own authoritative uses, and the
// local adopts their resolved type forward. Concrete-declared arithmetic (e.g.
// `b u16 :: a + 2`) still pushes, since value_type is the concrete declared type.
init_is_arith := is_arith_kind(checker.ast_module.exprs[statement.expr].kind)
if is_runtime_type(checker, declared_local) || !init_is_arith {
record_demand(checker, statement.expr, value_type, locals^[:], local_types, pkg, file)
}
case .Assignment:
value_type := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types)
// Only push the target's type back onto a bare-name RHS (e.g. `x += speed`):
// pushing through an arithmetic RHS would feed the target's (often provisional)
// type onto open-constant operands and poison their family. Operands of an
// arithmetic RHS resolve from their own authoritative uses.
rhs_is_arith := is_arith_kind(checker.ast_module.exprs[statement.expr].kind)
if statement.target != ast.INVALID_EXPR {
target_type := 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)
}
}
if !rhs_is_arith {
_ = record_demand(checker, statement.expr, target_type, locals^[:], local_types, pkg, file)
}
} else if statement.name != checker.sink_symbol {
if local_index, ok := find_infer_local_index(locals^[:], statement.name); ok &&
locals^[local_index].mutable {
_ = merge_infer_local_type(checker, &locals^[local_index], value_type, local_types)
if !rhs_is_arith {
_ = record_demand(checker, statement.expr, locals^[local_index].type, locals^[:], local_types, pkg, file)
}
}
}
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) {
_ = record_demand(checker, statement.expr, result_hint, locals^[:], local_types, pkg, file)
expr := checker.ast_module.exprs[statement.expr]
if expr.kind == .Name && !symbol.is_valid(expr.qualifier) {
if local_index, ok := find_infer_local_index(locals^[:], expr.name); ok {
// Open constants can adopt the result type cross-family; other
// locals widen within family as before.
if !merge_local_demand(checker, &locals^[local_index], result_hint, local_types) {
_ = merge_infer_local_type(checker, &locals^[local_index], result_hint, local_types)
}
returned = result_hint
} else {
// `return G` for a global const: demand the result type onto it.
record_demand(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
}
open_integer_accepts_demand :: proc(checker: ^Checker, value: i128, demand: types.Type) -> bool {
if types.is_concrete_integer(demand) {
return fits_integer_type(value, demand, checker.target)
}
return types.is_float(demand, checker.target)
}
open_float_accepts_demand :: proc(checker: ^Checker, demand: types.Type) -> bool {
return types.is_float(demand, checker.target)
}
// merge_open_const_demand records a concrete numeric demand onto an open numeric
// global's slot. Integer constants may adopt integer or float demands; float
// constants may adopt float demands. Later demands only widen within the chosen family.
merge_open_const_demand :: proc(
checker: ^Checker,
slot: ^types.Type,
demand: types.Type,
int_open: bool,
float_open: bool,
value: i128,
) -> bool {
if !(int_open && open_integer_accepts_demand(checker, value, demand) ||
float_open && open_float_accepts_demand(checker, demand)) {
return false
}
if !is_runtime_type(checker, slot^) {
slot^ = demand
return true
}
if types.equal(slot^, demand) {
return false
}
merged := types.widest(slot^, demand)
if types.is_concrete_scalar(merged) && !types.equal(slot^, merged) {
slot^ = merged
return true
}
return false
}
// merge_global_demand routes a concrete demand onto a global's slot: open constants
// adopt any fitting family, other referents widen within family. Sets a dirty flag so
// a demand pushed from a function body re-triggers the inference fixpoint.
merge_global_demand :: proc(checker: ^Checker, global: ast.Global_Id, demand: types.Type) -> bool {
index := int(global)
if index < 0 || index >= len(checker.global_demands) {
return false
}
changed: bool
if checker.global_open_const[index] || checker.global_open_float[index] {
changed = merge_open_const_demand(
checker,
&checker.global_demands[index],
demand,
checker.global_open_const[index],
checker.global_open_float[index],
checker.global_const_value[index],
)
} else {
changed = merge_inferred_type(&checker.module.types, &checker.global_demands[index], demand)
}
checker.global_demands_dirty = checker.global_demands_dirty || changed
return changed
}
// merge_local_demand records a concrete numeric demand onto an open-constant local.
// The first demand replaces the literal's default type; later demands may only widen
// within the chosen family.
merge_local_demand :: proc(checker: ^Checker, local: ^Infer_Local, demand: types.Type, local_types: []types.Type) -> bool {
if !(local.open_const && open_integer_accepts_demand(checker, local.const_value, demand) ||
local.open_float && open_float_accepts_demand(checker, demand)) {
return false
}
if types.is_constraint(local.declared) &&
!types.constraint_accepts(local.declared, demand, &checker.module.types) {
return false
}
if !local.demanded {
local.type = demand
local.demanded = true
record_infer_local_type(local^, local_types)
return true
}
if types.equal(local.type, demand) {
return false
}
merged := types.widest(local.type, demand)
if types.is_concrete_scalar(merged) && !types.equal(local.type, merged) {
local.type = merged
record_infer_local_type(local^, local_types)
return true
}
return false
}
is_arith_kind :: proc(k: ast.Expr_Kind) -> bool {
return k == .Add || k == .Sub || k == .Mul || k == .Div || k == .Negate
}
// open_const_default_type returns the fallback type a bare open-constant reference
// (local or global) would take if no use ever demands it: the smallest signed type
// that holds an integer constant, or f64 for a float constant. Used to give a call's
// argument a concrete type for monomorphization when deferred defaulting has left the
// open constant typeless (its param is a constraint, so the call records no demand on
// it). The fallback feeds only the specialization's arg vector, never a demand, so it
// cannot leak back onto the constant or a sibling.
open_const_default_type :: proc(
checker: ^Checker,
expr_id: ast.Expr_Id,
locals: []Infer_Local,
pkg: ast.Package_Id,
file: ast.File_Id,
) -> types.Type {
if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) {
return types.INVALID
}
expr := checker.ast_module.exprs[expr_id]
if expr.kind != .Name {
return types.INVALID
}
if !symbol.is_valid(expr.qualifier) {
if index, ok := find_infer_local_index(locals, expr.name); ok {
local := locals[index]
if local.open_const {
return types.smallest_signed_for_literal(i64(local.const_value))
}
if local.open_float {
return types.F64
}
return types.INVALID
}
}
target_pkg, available := expr_package(checker, expr, pkg, file)
if !available {
return types.INVALID
}
global := find_global(checker, expr.name, target_pkg)
index := int(global)
if global == ast.INVALID_GLOBAL || index < 0 || index >= len(checker.global_open_const) {
return types.INVALID
}
if checker.global_open_const[index] {
return types.smallest_signed_for_literal(i64(checker.global_const_value[index]))
}
if checker.global_open_float[index] {
return types.F64
}
return types.INVALID
}
// numeric_operand_is_open reports whether an arithmetic operand still carries a
// provisional type (an open constant at its smallest-signed default, or a bare
// numeric literal) rather than an authoritative one. A provisional operand must not
// propagate its type onto a sibling open constant: doing so locks the sibling into a
// default family and blocks the real backward demand from the declaration/use. Two
// provisional operands are instead resolved together by that backward demand.
numeric_operand_is_open :: proc(
checker: ^Checker,
expr_id: ast.Expr_Id,
locals: []Infer_Local,
pkg: ast.Package_Id,
file: ast.File_Id,
) -> bool {
if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) {
return false
}
expr := checker.ast_module.exprs[expr_id]
#partial switch expr.kind {
case .Name:
// A bare open-constant name is open iff it has a deferred default to assign.
return is_runtime_type(checker, open_const_default_type(checker, expr_id, locals, pkg, file))
case .Negate:
return numeric_operand_is_open(checker, expr.left, locals, pkg, file)
case .Add, .Sub, .Mul, .Div:
return numeric_operand_is_open(checker, expr.left, locals, pkg, file) ||
numeric_operand_is_open(checker, expr.right, locals, pkg, file)
}
// A bare integer/float literal adapts freely, so it too is provisional.
return is_numeric_constant_expr(checker, expr_id)
}
expr_accepts_numeric_demand :: proc(
checker: ^Checker,
expr_id: ast.Expr_Id,
demand: types.Type,
locals: []Infer_Local,
pkg: ast.Package_Id,
file: ast.File_Id,
) -> bool {
if !is_numeric_demand(demand, checker.target) ||
expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) {
return false
}
if constant := eval_constant(checker, expr_id); constant.kind == .Value {
return open_integer_accepts_demand(checker, constant.value, demand)
}
if is_float_constant_expr(checker, expr_id) {
return open_float_accepts_demand(checker, demand)
}
expr := checker.ast_module.exprs[expr_id]
#partial switch expr.kind {
case .Name:
if !symbol.is_valid(expr.qualifier) {
if index, ok := find_infer_local_index(locals, expr.name); ok {
local := locals[index]
return local.open_const && open_integer_accepts_demand(checker, local.const_value, demand) ||
local.open_float && open_float_accepts_demand(checker, demand)
}
}
target_pkg, available := expr_package(checker, expr, pkg, file)
if !available {
return false
}
global := find_global(checker, expr.name, target_pkg)
index := int(global)
if global == ast.INVALID_GLOBAL || index < 0 || index >= len(checker.global_open_const) {
return false
}
return checker.global_open_const[index] &&
open_integer_accepts_demand(checker, checker.global_const_value[index], demand) ||
checker.global_open_float[index] && open_float_accepts_demand(checker, demand)
case .Negate:
if !types.is_signed(demand, checker.target) && !types.is_float(demand, checker.target) {
return false
}
return expr_accepts_numeric_demand(checker, expr.left, demand, locals, pkg, file)
case .Add, .Sub, .Mul, .Div:
return expr_accepts_numeric_demand(checker, expr.left, demand, locals, pkg, file) &&
expr_accepts_numeric_demand(checker, expr.right, demand, locals, pkg, file)
}
return false
}
// record_demand pushes a concrete type demand onto open numeric slots reachable
// through bare names and numeric arithmetic. Calls remain a boundary (milestone 14.5).
record_demand :: proc(
checker: ^Checker,
expr_id: ast.Expr_Id,
demand: types.Type,
locals: []Infer_Local,
local_types: []types.Type,
pkg: ast.Package_Id,
file: ast.File_Id,
) -> bool {
if !is_runtime_type(checker, demand) ||
expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) {
return false
}
expr := checker.ast_module.exprs[expr_id]
#partial switch expr.kind {
case .Name:
if !symbol.is_valid(expr.qualifier) {
if index, ok := find_infer_local_index(locals, expr.name); ok {
return merge_local_demand(checker, &locals[index], demand, local_types)
}
}
target_pkg, available := expr_package(checker, expr, pkg, file)
if !available {
return false
}
global := find_global(checker, expr.name, target_pkg)
if global != ast.INVALID_GLOBAL {
return merge_global_demand(checker, global, demand)
}
case .Negate:
if types.is_signed(demand, checker.target) || types.is_float(demand, checker.target) {
return record_demand(checker, expr.left, demand, locals, local_types, pkg, file)
}
case .Add, .Sub, .Mul, .Div:
if is_numeric_demand(demand, checker.target) {
left := record_demand(checker, expr.left, demand, locals, local_types, pkg, file)
right := record_demand(checker, expr.right, demand, locals, local_types, pkg, file)
return left || right
}
}
return false
}
infer_all :: proc(checker: ^Checker) {
// An "open constant" global has no concrete declared type and a compile-time
// numeric initializer. Its slot can adopt a backward demand from any reachable use.
// Demands accumulate in global_demands so the default never blocks a later
// cross-family demand (e.g. integer literal -> unsigned or float).
for global, index in checker.ast_module.globals {
declared := 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
} else if is_float_constant_expr(checker, global.expr) {
checker.global_open_float[index] = true
}
}
main_template := find_template(checker, checker.main_symbol, 0)
if main_template != ast.INVALID_FUNCTION {
ensure_spec(checker, main_template, nil)
}
for {
changed := false
checker.global_demands_dirty = false
spec_count := len(checker.specs)
// Backward demands: a global pushes its own (declared or already-resolved) type
// onto open numeric slots reachable through names and numeric arithmetic.
for global, index in checker.ast_module.globals {
if global.external {
continue
}
demand := checker.global_types[index]
if !is_runtime_type(checker, demand) {
continue
}
_ = record_demand(checker, global.expr, demand, nil, nil, global.pkg, global.file)
}
// Forward / resolution. infer_expr runs for every non-external global (even
// concrete-typed ones) for its side effect of specializing called functions and
// recording demands from call arguments in their initializers.
for global, index in checker.ast_module.globals {
if global.external {
continue
}
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] || checker.global_open_float[index] {
// Defer defaulting: an undemanded open constant stays typeless during the
// fixpoint so its provisional smallest-signed default never leaks as a
// demand and poisons a sibling open constant used in the same arithmetic
// (e.g. the lagging type in `x += speed`). The pass after this loop assigns
// the default once the fixpoint settles and no further demand can arrive.
} else {
changed = merge_inferred_type(&checker.module.types, &checker.global_types[index], inferred) || changed
}
}
for index := 0; index < len(checker.specs); index += 1 {
id := spec_id(index)
inferred := infer_spec_result(checker, id)
changed = merge_inferred_type(&checker.module.types, &checker.specs[id].result, inferred) || changed
}
if len(checker.specs) != spec_count {
changed = true
}
// A demand pushed onto a global from inside a function body (via the spec loop)
// is picked up by the next pass's resolution, so keep iterating for it.
if checker.global_demands_dirty {
changed = true
}
if !changed {
break
}
}
// Any open constant that no use ever demanded now takes its default: an integer
// constant the smallest signed type that holds its value, a float constant f64.
for global, index in checker.ast_module.globals {
if global.external || is_runtime_type(checker, checker.global_types[index]) {
continue
}
if checker.global_open_const[index] {
checker.global_types[index] = types.smallest_signed_for_literal(i64(checker.global_const_value[index]))
} else if checker.global_open_float[index] {
checker.global_types[index] = types.F64
}
}
}
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(params) {
if index >= 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..<fixed_count {
stack[frame_index].built_args[index] = coerce_expr(
checker,
stack[frame_index].built_args[index],
checker.specs[spec].args[index],
checker.module.exprs[stack[frame_index].built_args[index]].span,
)
}
for index in fixed_count..<len(stack[frame_index].built_args) {
arg := stack[frame_index].built_args[index]
stack[frame_index].built_args[index] = promote_c_vararg_expr(
checker,
arg,
checker.module.exprs[arg].span,
)
}
function_id := checker.specs[spec].hir_id
assert(function_id != hir.INVALID_FUNCTION)
add_unique_function(calls, function_id)
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(stack[frame_index].built_args, checker.allocator)
stack[frame_index].built_args = nil
last = invalid_hir_expr(checker, expr.span, id)
} else {
last = add_hir_expr(checker, hir.Expr{
kind=.Call, span=expr.span, type=result, target=hir.function_ref(function_id),
left = hir.INVALID_EXPR, right = hir.INVALID_EXPR, args=stack[frame_index].built_args, diagnostic = source.INVALID_DIAGNOSTIC,
})
stack[frame_index].built_args = nil
}
_ = pop(&stack)
}
if frame.stage == 6 {
callee := last
_, function_item, function_type, ok := types.function_pointer(checker.module.exprs[callee].type, &checker.module.types)
if !ok {
id := source.add(checker.diagnostics, expr.span, "call target is not a function pointer")
last = invalid_hir_expr(checker, expr.span, id)
_ = pop(&stack)
continue
}
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 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..<min(fixed_count, len(stack[frame_index].built_args)) {
expected_arg := callable_arg_expected(function_type, function_item, &checker.module.types, index)
stack[frame_index].built_args[index] = coerce_expr(
checker,
stack[frame_index].built_args[index],
expected_arg,
checker.module.exprs[stack[frame_index].built_args[index]].span,
)
}
for index in fixed_count..<len(stack[frame_index].built_args) {
arg := stack[frame_index].built_args[index]
stack[frame_index].built_args[index] = promote_c_vararg_expr(
checker,
arg,
checker.module.exprs[arg].span,
)
}
result := function_item.child
if !types.is_valid(result) {
id := source.add(checker.diagnostics, expr.span, "could not resolve function pointer result type")
delete(stack[frame_index].built_args, checker.allocator)
stack[frame_index].built_args = nil
last = invalid_hir_expr(checker, expr.span, id)
} else {
last = add_hir_expr(checker, hir.Expr{
kind=.Call, span=expr.span, type=result, target=hir.INVALID_REF,
left=frame.left, right=hir.INVALID_EXPR, args=stack[frame_index].built_args,
diagnostic=source.INVALID_DIAGNOSTIC,
})
stack[frame_index].built_args = nil
}
_ = pop(&stack)
}
if frame.stage == 8 {
distinct_item, ok := types.node(&checker.module.types, frame.target_type)
actual := checker.module.exprs[last].type
if !ok || distinct_item.kind != .Distinct || !types.equal(actual, distinct_item.child) {
id := source.addf(
checker.diagnostics,
expr.span,
"distinct type '%s' requires an exact %s value, got %s",
symbol_text(checker, expr.name),
types.name(distinct_item.child),
types.name(actual),
)
last = invalid_hir_expr(checker, expr.span, id, frame.target_type)
} else {
last = add_hir_expr(checker, hir.Expr{
kind=.Retype,
span=expr.span,
type=frame.target_type,
left=last,
target=hir.INVALID_REF,
right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
_ = pop(&stack)
}
}
return last
}
make_link_name :: proc(checker: ^Checker, id: Spec_Id) -> 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 and inference carried useful numeric context: constraints,
// `undefined`, open numeric constants, or arithmetic expressions.
open_const_decl := !is_runtime_type(checker, declared) && !is_undefined_expr(checker, statement.expr)
if open_const_decl {
constant := eval_constant(checker, statement.expr)
open_const_decl = constant.kind == .Value && fits_i64(constant.value) ||
is_float_constant_expr(checker, statement.expr)
}
numeric_arithmetic_decl := !is_runtime_type(checker, declared) &&
is_numeric_arithmetic_expr(checker, statement.expr)
if statement_id != ast.INVALID_STMT && int(statement_id) < len(ctx.local_types) &&
is_runtime_type(checker, ctx.local_types[statement_id]) &&
(types.is_constraint(declared) || is_undefined_expr(checker, statement.expr) ||
open_const_decl || numeric_arithmetic_decl) {
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
}
ctx.loop_depth += 1
loop_body := build_block(ctx, statement.body)
ctx.loop_depth -= 1
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})
}
}
ctx.loop_depth += 1
loop_body := build_block(ctx, statement.body, capture_start)
ctx.loop_depth -= 1
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 .Break, .Continue:
if ctx.loop_depth == 0 {
keyword := "break" if statement.kind == .Break else "continue"
id := source.addf(checker.diagnostics, statement.span, "'%s' outside of a loop", keyword)
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = id,
})
ctx.problematic^ = true
continue
}
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Break if statement.kind == .Break else .Continue,
span = statement.span, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = source.INVALID_DIAGNOSTIC,
})
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:
// A literal `while true` makes the end of the block unreachable —
// unless its body can `break` out of this loop.
if statement.expr != hir.INVALID_EXPR && int(statement.expr) < len(module.exprs) {
condition := module.exprs[statement.expr]
if condition.kind == .Bool && condition.integer != 0 &&
!loop_body_breaks(module, statement.then_body) {
return true
}
}
}
}
return false
}
// Reports whether `stmts` contains a `break` that targets the enclosing loop:
// a `.Break` at this level or inside `if`/`else` branches counts, but a `break`
// inside a nested `.While`/`.For` targets that inner loop, so we do not descend.
loop_body_breaks :: proc(module: ^hir.Module, stmts: []hir.Stmt_Id) -> bool {
for id in stmts {
statement := module.statements[id]
#partial switch statement.kind {
case .Break:
return true
case .If:
if loop_body_breaks(module, statement.then_body) ||
loop_body_breaks(module, statement.else_body) {
return true
}
}
}
return false
}
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(&params, 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 integer constant: build against its demanded/defaulted type. Gated
// to the infer-side open-constant condition so out-of-range constants keep
// their original "exceeds signed i64 range" diagnostic.
expected = checker.global_types[global_index]
} else if (is_float_constant_expr(checker, global.expr) ||
is_numeric_arithmetic_expr(checker, global.expr)) &&
is_runtime_type(checker, checker.global_types[global_index]) {
expected = checker.global_types[global_index]
}
expr := build_expr(checker, global.expr, nil, &dependencies, &calls, expected, global.pkg, global.file)
global_type := checker.global_types[global_index]
if is_runtime_type(checker, declared) {
expr = coerce_expr(checker, expr, declared, global.span)
global_type = checker.module.exprs[expr].type
} else if is_runtime_type(checker, checker.module.exprs[expr].type) {
global_type = checker.module.exprs[expr].type
}
diagnostic := source.INVALID_DIAGNOSTIC
if !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_open_float = make([]bool, len(ast_module.globals), allocator)
checker.global_const_value = make([]i128, len(ast_module.globals), allocator)
checker.external_global_canonical = make([]ast.Global_Id, len(ast_module.globals), allocator)
checker.external_global_diagnostics = make([]source.Diagnostic_Id, len(ast_module.globals), allocator)
for &canonical in checker.external_global_canonical {
canonical = ast.INVALID_GLOBAL
}
for &diagnostic in checker.external_global_diagnostics {
diagnostic = source.INVALID_DIAGNOSTIC
}
checker.constants = make([]Constant, len(ast_module.exprs), allocator)
checker.template_diagnostics = make([]source.Diagnostic_Id, len(ast_module.functions), allocator)
for &diagnostic in checker.template_diagnostics {
diagnostic = source.INVALID_DIAGNOSTIC
}
defer {
for spec in checker.specs {
delete(spec.args, allocator)
}
delete(checker.specs)
delete(checker.function_index, allocator)
delete(checker.global_index, allocator)
delete(checker.import_index, allocator)
delete(checker.global_types, allocator)
delete(checker.global_demands, allocator)
delete(checker.global_open_const, allocator)
delete(checker.global_open_float, allocator)
delete(checker.global_const_value, allocator)
delete(checker.external_global_canonical, allocator)
delete(checker.external_global_diagnostics, allocator)
delete(checker.constants, allocator)
delete(checker.template_diagnostics, allocator)
delete(checker.constant_stack)
delete(checker.ast_expr_stack)
delete(checker.hir_expr_stack)
delete(checker.infer_stack)
delete(checker.build_stack)
delete(checker.cycle_stack)
}
for 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
}