Files
brolang/compiler/checker/checker.odin
T
2026-08-09 15:08:20 +02:00

14851 lines
546 KiB
Odin

package checker
import "../ast"
import "../hir"
import "../source"
import "../symbol"
import "../target"
import "../types"
import "core:fmt"
import "core:mem"
import "core:slice"
import "core:strings"
Spec_Id :: distinct u32
INVALID_SPEC :: Spec_Id(0xffff_ffff)
spec_id :: proc(index: int) -> Spec_Id {
assert(index >= 0 && u64(index) < u64(INVALID_SPEC))
return Spec_Id(index)
}
spec_index :: proc(id: Spec_Id, count: int) -> (int, bool) {
index := int(id)
return index, id != INVALID_SPEC && index < count
}
Spec :: struct {
template: ast.Function_Id,
args: []types.Type,
comptime_values: []Comptime_Value,
result: types.Type,
hir_id: hir.Function_Id,
}
Infer_Local :: struct {
name: symbol.Id,
type: types.Type,
declared: types.Type,
statement: ast.Stmt_Id,
mutable: bool,
// Open numeric constants and value sources can adopt a backward demand from use.
open_const: bool,
open_float: bool,
open_value_source: bool,
const_value: i128,
demanded: bool,
}
Build_Local :: struct {
name: symbol.Id,
type: types.Type,
mutable: bool,
id: hir.Local_Id,
}
Static_Binding :: struct {
name: symbol.Id,
type: types.Type,
value: Ct_Value_Id,
}
Expand_Expansion :: struct {
statement: ast.Stmt_Id,
index: u32,
}
Entry_Point_Kind :: enum u8 {
Invalid,
Plain,
Process,
}
// Build_Ctx threads the per-function accumulators through build_block so that
// nested control-flow blocks (if/else) can be built recursively. `locals` is a
// scope stack: each block records its entry length and truncates back to it on
// exit, while `hir_locals` keeps every allocated slot for the function.
// A labeled value-loop currently being built. A `yield :label x` inside the loop
// body assigns `x` to the loop's result `slot` (typed `slot_type`) and `break`s.
// Pushed by `build_value_loop` while its body is built; innermost is last.
Yield_Target :: struct {
label: symbol.Id,
slot: hir.Local_Id,
slot_type: types.Type,
// True when the loop also yields `null` (a `{T, null}` set → `?T`); set from a
// pure-AST scan, used to pick the slot's element type on the first concrete yield.
result_optional: bool,
// `len(defers)` when this target's body began; a `yield :label` flushes defers down
// to here before breaking, so an outer-loop / value-block yield runs inner defers too.
defer_floor: int,
}
Defer_Entry :: struct {
body: []hir.Stmt_Id,
error_only: bool,
capture: hir.Local_Id,
}
Error_Refinement :: struct {
local: hir.Local_Id,
variants: []u32,
}
Build_Ctx :: struct {
checker: ^Checker,
pkg: ast.Package_Id,
file: ast.File_Id,
result: types.Type,
local_types: []types.Type,
locals: ^[dynamic]Build_Local,
hir_locals: ^[dynamic]hir.Local,
local_spans: ^[dynamic]source.Span,
local_used: ^[dynamic]bool,
local_warnable: ^[dynamic]bool,
global_reads: ^[dynamic]hir.Global_Id,
calls: ^[dynamic]hir.Function_Id,
problematic: ^bool,
error_refinements: ^[dynamic]Error_Refinement,
// Stack of labeled value-loops being built (innermost last); see Yield_Target.
yield_targets: ^[dynamic]Yield_Target,
// `defer` lowering. Deferred statements are built once at the `defer` site and
// their hir stmt ids stored here as a flat stack across scopes (one entry per
// deferred statement); they are replayed (appended) at each scope exit in LIFO
// order. `loop_defer_starts` records `len(defers)` at each enclosing loop body
// entry: `break`/`continue` flush down to that mark (and need `len > loop_floor`
// to be valid). `defer_depth`/`loop_floor` guard control flow inside a deferred
// statement: `return` is rejected while `defer_depth > 0`, and `break`/`continue`
// only see loops opened within the defer (those past `loop_floor`).
defers: ^[dynamic]Defer_Entry,
loop_defer_starts: ^[dynamic]int,
// Parallel to `loop_defer_starts`: the label of each enclosing break target (INVALID
// when unlabeled), so a `break :L` / `continue :L` can target an outer one. A labeled
// block statement is a break target too; `loop_is_loop` distinguishes loops (which
// `continue` and unlabeled `break`/`continue` target) from value/labeled blocks.
loop_labels: ^[dynamic]symbol.Id,
loop_is_loop: ^[dynamic]bool,
defer_depth: int,
loop_floor: int,
}
Function_Index_Entry :: struct {
scope: ast.Package_Id,
hidden: bool,
name: symbol.Id,
id: ast.Function_Id,
}
Global_Index_Entry :: struct {
scope: ast.Package_Id,
hidden: bool,
name: symbol.Id,
id: ast.Global_Id,
}
Import_Index_Entry :: struct {
scope: ast.File_Id,
name: symbol.Id,
id: ast.Import_Id,
}
Type_Factory_Entry :: struct {
template: ast.Function_Id,
values: []Comptime_Value,
result: types.Type,
resolving: bool,
}
Generated_Type_Entry :: struct {
expr: ast.Expr_Id,
values: []Comptime_Value,
result: types.Type,
pkg: ast.Package_Id,
file: ast.File_Id,
defaults: []Ct_Value_Id,
}
Resolved_Field_Default :: struct {
expr: ast.Expr_Id,
pkg: ast.Package_Id,
file: ast.File_Id,
static_value: Ct_Value_Id,
span: source.Span,
}
Type_Factory_Origin :: struct {
result: types.Type,
template: ast.Function_Id,
values: []Comptime_Value,
}
Call_Fold :: enum u8 {
Unknown,
Runtime,
Value,
Compile_Error,
}
Call_Resolution :: struct {
expr: ast.Expr_Id,
ctx: []Comptime_Value,
expand_ctx: []Expand_Expansion,
mapping: []int,
comptime_values: []Comptime_Value,
runtime_types: []types.Type,
fold: Call_Fold,
folded_value: Ct_Value_Id,
diagnostic: source.Diagnostic_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,
// Program-wide inference for direct constraint fields in named native records.
// The arrays use Store.fields' existing dense indices; resolved concrete types
// live directly in module.types.fields so layout and lowering need no side table.
record_field_constraints: []types.Type,
record_field_defaults: []types.Type,
record_field_conflicts: []types.Type,
record_field_conflict_spans: []source.Span,
record_field_demands_dirty: bool,
poisoned_packages: []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,
hir_expr_stack: [dynamic]hir.Expr_Id,
infer_stack: [dynamic]Infer_Frame,
build_stack: [dynamic]Build_Expr_Frame,
cycle_stack: [dynamic]Cycle_Frame,
// Anonymous globals synthesized for `&<array literal>` (Zig's `&.{...}`). Staged
// here during global/function building and flushed into module.globals AFTER
// build_globals, so the 1:1 module.globals <-> ast.globals index identity holds.
anon_globals: [dynamic]hir.Global,
main_symbol: symbol.Id,
entry_point: Entry_Point_Kind,
io_provider_template: ast.Function_Id,
sink_symbol: symbol.Id,
type_symbol: symbol.Id,
current_result: types.Type,
inferred_test_error: ^types.Type,
current_build_ctx: ^Build_Ctx,
current_comptime_values: []Comptime_Value,
static_state: Ct_State,
static_bindings: [dynamic]Static_Binding,
comptime_keys: [dynamic]string,
comptime_static_values: [dynamic]Ct_Value_Id,
expand_context: [dynamic]Expand_Expansion,
type_factories: [dynamic]Type_Factory_Entry,
generated_types: [dynamic]Generated_Type_Entry,
type_factory_origins: [dynamic]Type_Factory_Origin,
call_resolutions: [dynamic]Call_Resolution,
building_hir: bool,
target: target.Target,
allocator: mem.Allocator,
}
symbol_text :: proc(checker: ^Checker, id: symbol.Id) -> string {
return symbol.resolve(checker.symbols, id)
}
append_tracked_local :: proc(
locals: ^[dynamic]hir.Local,
spans: ^[dynamic]source.Span,
used: ^[dynamic]bool,
warnable: ^[dynamic]bool,
local: hir.Local,
span: source.Span,
) -> hir.Local_Id {
id := hir.local_id(len(locals^))
append(locals, local)
append(spans, span)
append(used, false)
append(warnable, true)
return id
}
append_build_local :: proc(
ctx: ^Build_Ctx,
name: symbol.Id,
type: types.Type,
mutable: bool,
span: source.Span,
) -> hir.Local_Id {
id := append_tracked_local(
ctx.hir_locals,
ctx.local_spans,
ctx.local_used,
ctx.local_warnable,
hir.Local{name=name, type=type, mutable=mutable},
span,
)
append(ctx.locals, Build_Local{name=name, type=type, mutable=mutable, id=id})
return id
}
ignore_tracked_locals :: proc(ctx: ^Build_Ctx, start: int) {
for i := start; i < len(ctx.local_warnable^); i += 1 {
ctx.local_warnable^[i] = false
}
}
mark_local_used :: proc(checker: ^Checker, id: hir.Local_Id) {
ctx := checker.current_build_ctx
if ctx == nil || id == hir.INVALID_LOCAL {
return
}
index := int(id)
if index >= 0 && index < len(ctx.local_used^) {
ctx.local_used^[index] = true
}
}
build_local_expr :: proc(checker: ^Checker, local: Build_Local, span: source.Span) -> hir.Expr_Id {
mark_local_used(checker, local.id)
return add_hir_expr(checker, hir.Expr{
kind=.Local, span=span, type=local.type, target=hir.local_ref(local.id),
left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC,
})
}
block_reads_name :: proc(checker: ^Checker, statements: []ast.Stmt_Id, name: symbol.Id) -> bool {
// Imported C expressions can be deeply nested, so walk the source graph iteratively.
statement_stack: [dynamic]ast.Stmt_Id
statement_stack.allocator = checker.allocator
defer delete(statement_stack)
expr_stack: [dynamic]ast.Expr_Id
expr_stack.allocator = checker.allocator
defer delete(expr_stack)
append(&statement_stack, ..statements)
for len(statement_stack) > 0 || len(expr_stack) > 0 {
if len(statement_stack) > 0 {
statement_id := pop(&statement_stack)
if statement_id == ast.INVALID_STMT || int(statement_id) >= len(checker.ast_module.statements) {
continue
}
statement := checker.ast_module.statements[statement_id]
#partial switch statement.kind {
case .Declaration, .Assignment, .Return, .Expression, .Yield:
append(&expr_stack, statement.expr, statement.target)
append(&statement_stack, ..statement.body)
case .If:
append(&expr_stack, statement.expr, statement.guard)
append(&statement_stack, ..statement.body)
append(&statement_stack, ..statement.else_body)
case .While:
append(&expr_stack, statement.expr)
append(&statement_stack, ..statement.body)
if statement.update != ast.INVALID_STMT {
append(&statement_stack, statement.update)
}
case .For:
append(&expr_stack, statement.expr)
append(&statement_stack, ..statement.body)
case .Block:
append(&statement_stack, ..statement.body)
case .Defer:
if statement.update != ast.INVALID_STMT {
append(&statement_stack, statement.update)
}
case .Match, .Match_Arm:
append(&expr_stack, statement.expr)
append(&expr_stack, ..statement.patterns)
append(&statement_stack, ..statement.body)
case .Break, .Continue, .Invalid:
}
continue
}
expr_id := pop(&expr_stack)
if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) {
continue
}
expr := checker.ast_module.exprs[expr_id]
if expr.kind == .Name || expr.kind == .Call {
if !symbol.is_valid(expr.qualifier) && expr.name == name || expr.qualifier == name {
return true
}
}
switch expr.kind {
case .Call, .Array, .Struct_Literal, .Slice:
append(&expr_stack, ..expr.args)
append(&expr_stack, expr.left)
case .Negate, .Not, .Bit_Not, .Address, .Deref, .Field, .Unwrap, .Try, .Keyed, .Enum_Literal, .Cast:
append(&expr_stack, expr.left)
case .Comptime:
append(&expr_stack, expr.left)
append(&statement_stack, ..expr.body)
case .Catch:
append(&expr_stack, expr.left, expr.right)
append(&statement_stack, ..expr.body)
case .Add, .Sub, .Mul, .Div, .Bit_And, .Bit_Or, .Bit_Xor, .Shift_Left, .Shift_Right,
.Shift_Left_Saturating, .Index, .Orelse, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range:
append(&expr_stack, expr.left, expr.right)
case .Invalid, .Integer, .Float, .String, .Bool, .Null, .Unreachable, .Undefined, .Inference_Hole,
.Type, .Name, .Function_Literal, .Anonymous_Struct_Type:
}
}
return false
}
record_unused_locals :: proc(
checker: ^Checker,
locals: []hir.Local,
spans: []source.Span,
used: []bool,
warnable: []bool,
) {
for local, index in locals {
if local.name == checker.sink_symbol ||
!symbol.is_valid(local.name) ||
index >= len(warnable) ||
!warnable[index] ||
index >= len(used) ||
used[index] {
continue
}
span := source.Span{}
if index < len(spans) {
span = spans[index]
}
if local.parameter {
source.addf_warning(checker.diagnostics, span, "unused parameter '%s'", symbol_text(checker, local.name))
} else {
source.addf_warning(checker.diagnostics, span, "unused local '%s'", symbol_text(checker, local.name))
}
}
}
write_type_label :: proc(checker: ^Checker, builder: ^strings.Builder, value: types.Type) {
store := &checker.module.types
item, ok := types.node(store, value)
if !ok {
strings.write_string(builder, types.name(value))
return
}
if item.name != 0 {
strings.write_string(builder, symbol_text(checker, symbol.Id(item.name)))
return
}
switch item.kind {
case .Array:
strings.write_byte(builder, '[')
if item.inferred_count {
strings.write_byte(builder, '_')
} else {
fmt.sbprintf(builder, "%d", item.count)
}
if item.has_sentinel {
fmt.sbprintf(builder, ";%d", item.sentinel)
}
strings.write_byte(builder, ']')
if item.mutable {
strings.write_string(builder, "mut ")
}
write_type_label(checker, builder, item.child)
case .Pointer:
if item.has_sentinel {
fmt.sbprintf(builder, "[*;%d]", item.sentinel)
} else {
strings.write_byte(builder, '*' if item.many else '@')
}
if item.mutable {
strings.write_string(builder, "mut ")
}
write_type_label(checker, builder, item.child)
case .Slice:
if item.has_sentinel {
fmt.sbprintf(builder, "[;%d]", item.sentinel)
} else {
strings.write_string(builder, "[]")
}
if item.mutable {
strings.write_string(builder, "mut ")
}
write_type_label(checker, builder, item.child)
case .Range:
strings.write_string(builder, "range(")
write_type_label(checker, builder, item.child)
strings.write_byte(builder, ')')
case .Optional:
strings.write_byte(builder, '?')
write_type_label(checker, builder, item.child)
case .Function:
strings.write_string(builder, "c_func(" if item.c_abi else "func(")
for param, index in types.params_for(store, value) {
if index > 0 {
strings.write_string(builder, ", ")
}
write_type_label(checker, builder, param.type)
}
if item.variadic {
if item.field_count > 0 {
strings.write_string(builder, ", ")
}
strings.write_string(builder, "...")
}
strings.write_string(builder, ") ")
write_type_label(checker, builder, item.child)
case .Fallible:
write_type_label(checker, builder, item.child)
strings.write_string(builder, " ! ")
write_type_label(checker, builder, item.extra)
case .Sum:
write_type_label(checker, builder, item.child)
strings.write_string(builder, " | ")
write_type_label(checker, builder, item.extra)
case .Type_Call:
strings.write_string(builder, "<type factory call>")
case .Struct:
strings.write_string(builder, "struct")
case .Union:
strings.write_string(builder, "union")
case .Enum:
strings.write_string(builder, "enum")
case .Alias, .Distinct, .Named:
write_type_label(checker, builder, item.child)
case .Invalid, .Void, .Noreturn, .Anyopaque, .Int_Constraint, .Uint_Constraint, .Float_Constraint, .Range_Constraint, .Scalar:
strings.write_string(builder, types.name(value))
}
}
// Render dynamic types using source syntax so diagnostics never expose internal
// type-store ids such as `<type 230>`.
type_label :: proc(checker: ^Checker, value: types.Type) -> string {
builder := strings.builder_make(context.temp_allocator)
write_type_label(checker, &builder, value)
return strings.to_string(builder)
}
is_ptrcast_call :: proc(checker: ^Checker, expr: ast.Expr) -> bool {
return expr.intrinsic &&
expr.left == ast.INVALID_EXPR &&
!symbol.is_valid(expr.qualifier) &&
symbol_text(checker, expr.name) == "ptrcast"
}
is_intrinsic_call :: proc(checker: ^Checker, expr: ast.Expr, name: string) -> bool {
return expr.intrinsic && expr.kind == .Call && expr.left == ast.INVALID_EXPR &&
!symbol.is_valid(expr.qualifier) && symbol_text(checker, expr.name) == name
}
static_field_value :: proc(checker: ^Checker, base, name: symbol.Id) -> (Ct_Value, bool) {
binding, ok := current_static_binding(checker, base)
if !ok || binding.value == INVALID_CT_VALUE || int(binding.value) >= len(checker.static_state.values) {
return {}, false
}
return persistent_field_value(checker, binding.value, name)
}
persistent_field_value :: proc(checker: ^Checker, root: Ct_Value_Id, name: symbol.Id) -> (Ct_Value, bool) {
if root == INVALID_CT_VALUE || int(root) >= len(checker.static_state.values) {
return {}, false
}
value := checker.static_state.values[root]
index, _, found := find_struct_field(checker, value.type, name)
children := ct_child_slice(&checker.static_state, value)
if !found || index < 0 || index >= len(children) || children[index] == INVALID_CT_VALUE ||
int(children[index]) >= len(checker.static_state.values) {
return {}, false
}
return checker.static_state.values[children[index]], true
}
build_static_value :: proc(checker: ^Checker, value: Ct_Value, span: source.Span, expected: types.Type) -> hir.Expr_Id {
if value.kind == .Void {
return add_hir_expr(checker, hir.Expr{
kind=.Void, span=span, type=types.VOID,
target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
if value.kind == .Integer {
if types.is_enum(value.type, &checker.module.types) {
return add_hir_expr(checker, hir.Expr{
kind=.Integer, span=span, type=value.type, integer=i64(value.integer),
target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
expr := ast.Expr{
kind=.Integer, span=span, integer=u64(value.integer),
left=ast.INVALID_EXPR, right=ast.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC,
}
return build_constant_expr(checker, expr, Constant{kind=.Value, value=value.integer}, value.type)
}
if value.kind == .Float {
bits := transmute(i64)value.float
if types.bits(value.type, checker.target) == 32 {
bits = i64(transmute(u32)f32(value.float))
}
return add_hir_expr(checker, hir.Expr{
kind=.Float, span=span, type=value.type, integer=bits,
target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
if value.kind == .Bool {
return add_hir_expr(checker, hir.Expr{
kind=.Bool, span=span, type=types.BOOL, integer=i64(value.integer),
target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
if value.kind == .String && value.index < u64(len(checker.ast_module.strings)) {
return add_hir_expr(checker, hir.Expr{
kind=.String, span=span, type=string_literal_type(checker, value.index), integer=i64(value.index),
target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
if value.kind == .Function {
value_expected := expected if types.is_valid(expected) else value.type
return build_function_value(checker, ast.Function_Id(u32(value.index)), span, value_expected)
}
if value.kind == .Array || value.kind == .Struct || value.kind == .Range {
children := ct_child_slice(&checker.static_state, value)
args := make([]hir.Expr_Id, len(children), checker.allocator)
for child, index in children {
if child == INVALID_CT_VALUE || int(child) >= len(checker.static_state.values) {
delete(args, checker.allocator)
id := source.add(checker.diagnostics, span, "invalid persistent compile-time aggregate")
return invalid_hir_expr(checker, span, id, expected)
}
args[index] = build_static_value(checker, checker.static_state.values[child], span, types.INVALID)
}
kind := hir.Expr_Kind.Array
if value.kind == .Struct {
kind = .Struct
} else if value.kind == .Range {
kind = .Range
}
return add_hir_expr(checker, hir.Expr{
kind=kind, span=span, type=value.type, integer=value.active, args=args,
target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
if value.kind == .Null {
return add_hir_expr(checker, hir.Expr{
kind=.Null, span=span, type=value.type,
target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
if value.kind == .Optional_Some {
children := ct_child_slice(&checker.static_state, value)
if len(children) == 1 && children[0] != INVALID_CT_VALUE &&
int(children[0]) < len(checker.static_state.values) {
child_type := types.child_type(value.type, &checker.module.types)
child := build_static_value(checker, checker.static_state.values[children[0]], span, child_type)
return add_hir_expr(checker, hir.Expr{
kind=.Optional_Some, span=span, type=value.type, left=child,
target=hir.INVALID_REF, right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
}
id := source.add(checker.diagnostics, span, "compile-time-only value cannot be used at runtime")
return invalid_hir_expr(checker, span, id, expected)
}
push_static_integer_binding :: proc(checker: ^Checker, name: symbol.Id, value_type: types.Type, value: i128) -> int {
start := len(checker.static_bindings)
if symbol.is_valid(name) && name != checker.sink_symbol {
id := ct_add_value(&checker.static_state, Ct_Value{kind=.Integer, type=value_type, integer=value})
append(&checker.static_bindings, Static_Binding{name=name, type=value_type, value=id})
}
return start
}
push_static_void_binding :: proc(checker: ^Checker, name: symbol.Id) -> int {
start := len(checker.static_bindings)
if symbol.is_valid(name) && name != checker.sink_symbol {
id := ct_add_value(&checker.static_state, Ct_Value{kind=.Void, type=types.VOID})
append(&checker.static_bindings, Static_Binding{name=name, type=types.VOID, value=id})
}
return start
}
pop_static_bindings :: proc(checker: ^Checker, start: int) {
resize(&checker.static_bindings, start)
}
comptime_string_argument :: proc(
checker: ^Checker,
id: ast.Expr_Id,
pkg: ast.Package_Id,
file: ast.File_Id,
) -> (string, bool) {
if id == ast.INVALID_EXPR || int(id) >= len(checker.ast_module.exprs) {
return "", false
}
expr := checker.ast_module.exprs[id]
if expr.kind == .String && expr.integer < u64(len(checker.ast_module.strings)) {
return checker.ast_module.strings[expr.integer], true
}
if expr.kind == .Name && !symbol.is_valid(expr.qualifier) {
value, ok := current_comptime_value(checker, expr.name)
if ok && value.kind == .String {
return value.text, true
}
}
if expr.kind == .Name && symbol.is_valid(expr.qualifier) &&
symbol_text(checker, expr.name) == "name" {
if value, ok := static_field_value(checker, expr.qualifier, expr.name); ok &&
value.kind == .String && value.index < u64(len(checker.ast_module.strings)) {
return checker.ast_module.strings[value.index], true
}
}
state := ct_state_make(checker, pkg, file, diagnose=false)
defer ct_state_destroy(&state)
value, flow, ok := ct_eval_expr(&state, id, types.INVALID, 0)
if ok && flow.kind == .Normal {
return ct_value_bytes(&state, value)
}
return "", false
}
comptime_slice_bytes :: proc(
checker: ^Checker,
expr: ast.Expr,
pkg: ast.Package_Id,
file: ast.File_Id,
) -> (string, bool) {
state := ct_state_make(checker, pkg, file, diagnose=false)
defer ct_state_destroy(&state)
value, flow, ok := ct_eval_slice_expr(&state, expr, 0)
if !ok || flow.kind != .Normal {
return "", false
}
return ct_value_bytes(&state, value)
}
canonical_decimal_index :: proc(text: string) -> (u64, bool) {
if len(text) == 0 || len(text) > 1 && text[0] == '0' {
return 0, false
}
value: u64
for byte in text {
if byte < '0' || byte > '9' {
return 0, false
}
digit := u64(byte-'0')
if value > (0xffff_ffff_ffff_ffff-digit)/10 {
return 0, false
}
value = value*10+digit
}
return value, true
}
field_intrinsic_expr :: proc(
checker: ^Checker,
expr: ast.Expr,
pkg: ast.Package_Id,
file: ast.File_Id,
) -> (ast.Expr, bool) {
if !is_intrinsic_call(checker, expr, "field") || len(expr.args) != 2 {
return {}, false
}
name, ok := comptime_string_argument(checker, expr.args[1], pkg, file)
if !ok {
return {}, false
}
result := ast.Expr{
kind=.Field, span=expr.span, left=expr.args[0], right=ast.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
}
if index, numeric := canonical_decimal_index(name); numeric {
result.integer = index
result.name = symbol.INVALID
} else {
result.name = symbol.intern(checker.symbols, name)
}
return result, true
}
std_named_type :: proc(checker: ^Checker, path, name: string) -> types.Type {
name_id := symbol.intern(checker.symbols, name)
result := types.INVALID
for &import_item in checker.ast_module.imports {
if import_item.valid && import_item.path == path {
import_item.used = true
if !types.is_valid(result) {
result = types.find_named(&checker.module.types, u32(import_item.target), u32(name_id))
}
}
}
return result
}
Type_Builtin :: enum u8 {
None,
Size_Of,
Align_Of,
Min_Value,
Max_Value,
}
Division_Builtin :: enum u8 {
None,
Trunc,
Floor,
Exact,
Ceil,
Rem,
Mod,
}
Memory_Builtin :: enum u8 {
None,
Copy,
Set,
}
memory_builtin_call :: proc(checker: ^Checker, expr: ast.Expr) -> Memory_Builtin {
if !expr.intrinsic || expr.kind != .Call || expr.left != ast.INVALID_EXPR || symbol.is_valid(expr.qualifier) {
return .None
}
switch symbol_text(checker, expr.name) {
case "memcopy": return .Copy
case "memset": return .Set
}
return .None
}
memory_region_type :: proc(checker: ^Checker, value: types.Type) -> (child: types.Type, mutable: bool, ok: bool) {
store := &checker.module.types
resolved := types.resolve_alias(value, store)
if item, item_ok := types.node(store, resolved); item_ok && item.kind == .Slice {
return item.child, item.mutable, true
}
pointer, pointer_ok := types.node(store, resolved)
if pointer_ok && pointer.kind == .Pointer && !pointer.many {
array, array_ok := types.node(store, types.resolve_alias(pointer.child, store))
if array_ok && array.kind == .Array {
return array.child, pointer.mutable && array.mutable, true
}
}
return types.INVALID, false, false
}
infer_memory_builtin :: proc(
checker: ^Checker,
expr: ast.Expr,
kind: Memory_Builtin,
locals: []Infer_Local,
pkg: ast.Package_Id,
file: ast.File_Id,
demanded: ^[dynamic]Spec_Id,
local_types: []types.Type,
) -> types.Type {
if len(expr.args) != 2 {
return types.INVALID
}
destination := infer_nested_expr(checker, expr.args[0], locals, pkg, file, demanded, local_types)
child, _, ok := memory_region_type(checker, destination)
if !ok {
_ = infer_nested_expr(checker, expr.args[1], locals, pkg, file, demanded, local_types)
return types.INVALID
}
if kind == .Set {
if !is_undefined_expr(checker, expr.args[1]) {
_ = infer_nested_expr(checker, expr.args[1], locals, pkg, file, demanded, local_types, child)
}
} else {
_ = infer_nested_expr(checker, expr.args[1], locals, pkg, file, demanded, local_types)
}
return types.VOID
}
division_builtin_call :: proc(checker: ^Checker, expr: ast.Expr) -> Division_Builtin {
if !expr.intrinsic || expr.kind != .Call || expr.left != ast.INVALID_EXPR || symbol.is_valid(expr.qualifier) {
return .None
}
switch symbol_text(checker, expr.name) {
case "divtrunc": return .Trunc
case "divfloor": return .Floor
case "divexact": return .Exact
case "divceil": return .Ceil
case "rem": return .Rem
case "mod": return .Mod
}
return .None
}
type_builtin_call :: proc(checker: ^Checker, expr: ast.Expr) -> Type_Builtin {
if !expr.intrinsic || expr.kind != .Call || expr.left != ast.INVALID_EXPR || symbol.is_valid(expr.qualifier) {
return .None
}
name := symbol_text(checker, expr.name)
if name == "sizeof" {
return .Size_Of
}
if name == "alignof" {
return .Align_Of
}
if name == "minval" {
return .Min_Value
}
if name == "maxval" {
return .Max_Value
}
return .None
}
valid_ptrcast_child :: proc(checker: ^Checker, value: types.Type) -> bool {
return types.is_valid(value) &&
!types.is_void(value) &&
!types.is_anyopaque(value) &&
!types.is_function(value, &checker.module.types) &&
(types.is_runtime_value(value, &checker.module.types) ||
types.is_opaque_struct(value, &checker.module.types))
}
valid_layout_type :: proc(checker: ^Checker, value: types.Type) -> bool {
return types.is_runtime_value(value, &checker.module.types)
}
is_integer_bound_type :: proc(checker: ^Checker, value: types.Type) -> bool {
representation := value
if backing, ok := types.distinct_scalar_backing(value, &checker.module.types); ok {
representation = backing
}
return types.is_concrete_integer(representation)
}
type_builtin_value :: proc(checker: ^Checker, kind: Type_Builtin, value: types.Type) -> i128 {
#partial switch kind {
case .Size_Of:
return i128(types.size(value, &checker.module.types, checker.target))
case .Align_Of:
return i128(types.alignment_of(value, &checker.module.types, checker.target))
case .Min_Value:
representation := value
if backing, ok := types.distinct_scalar_backing(value, &checker.module.types); ok {
representation = backing
}
if types.is_unsigned(representation, checker.target) {
return 0
}
return -(i128(1) << u32(types.bits(representation, checker.target)-1))
case .Max_Value:
representation := value
if backing, ok := types.distinct_scalar_backing(value, &checker.module.types); ok {
representation = backing
}
bit_count := types.bits(representation, checker.target)
sign_bit_count := 1 if types.is_signed(representation, checker.target) else 0
return (i128(1) << u32(bit_count-sign_bit_count))-1
case:
return 0
}
}
build_type_builtin :: proc(
checker: ^Checker,
expr: ast.Expr,
kind: Type_Builtin,
pkg: ast.Package_Id,
file: ast.File_Id,
) -> hir.Expr_Id {
if len(expr.args) != 1 {
id := source.addf(checker.diagnostics, expr.span, "%s! expects 1 argument, got %d", symbol_text(checker, expr.name), len(expr.args))
return invalid_hir_expr(checker, expr.span, id, types.USIZE)
}
target, target_ok := resolve_type_argument(checker, expr.args[0], pkg, file)
if !target_ok {
label := "layout" if kind == .Size_Of || kind == .Align_Of else "integer bound"
id := source.addf(checker.diagnostics, checker.ast_module.exprs[expr.args[0]].span, "%s target must be a type", label)
return invalid_hir_expr(checker, expr.span, id, types.USIZE)
}
if (kind == .Size_Of || kind == .Align_Of) && !valid_layout_type(checker, target) {
id := source.addf(checker.diagnostics, checker.ast_module.exprs[expr.args[0]].span, "layout target must be a sized runtime value type, got %s", type_label(checker, target))
return invalid_hir_expr(checker, expr.span, id, types.USIZE)
}
if (kind == .Min_Value || kind == .Max_Value) && !is_integer_bound_type(checker, target) {
id := source.addf(checker.diagnostics, checker.ast_module.exprs[expr.args[0]].span, "integer bound target must be a concrete integer type, got %s", type_label(checker, target))
return invalid_hir_expr(checker, expr.span, id, types.USIZE)
}
result_type := types.USIZE if kind == .Size_Of || kind == .Align_Of else target
_, distinct_ok := types.distinct_scalar_backing(result_type, &checker.module.types)
return build_constant_expr(
checker,
expr,
Constant{kind=.Value, value=type_builtin_value(checker, kind, target)},
result_type,
distinct_ok,
)
}
tag_result_type :: proc(checker: ^Checker, value: types.Type) -> (types.Type, bool) {
if !types.is_tagged_union(value, &checker.module.types) {
return types.INVALID, false
}
return types.union_tag_enum(value, &checker.module.types), true
}
enum_member_name_from_value :: proc(checker: ^Checker, enum_type: types.Type, value: i128) -> (string, bool) {
if !types.is_enum(enum_type, &checker.module.types) {
return "", false
}
for member in types.enum_members_for(&checker.module.types, enum_type) {
if member.value == value {
return symbol_text(checker, symbol.Id(member.name)), true
}
}
return "", false
}
build_tag_intrinsic :: proc(
checker: ^Checker,
expr: ast.Expr,
locals: []Build_Local,
global_reads: ^[dynamic]hir.Global_Id,
calls: ^[dynamic]hir.Function_Id,
pkg: ast.Package_Id,
file: ast.File_Id,
) -> hir.Expr_Id {
if len(expr.args) != 1 {
id := source.addf(checker.diagnostics, expr.span, "tag! expects 1 argument, got %d", len(expr.args))
return invalid_hir_expr(checker, expr.span, id)
}
state := ct_state_make(checker, pkg, file, values=checker.current_comptime_values, diagnose=false)
value_id, flow, comptime_ok := ct_eval_expr(&state, expr.args[0], types.INVALID, 0)
if comptime_ok && flow.kind == .Normal && value_id != INVALID_CT_VALUE && int(value_id) < len(state.values) {
value := state.values[value_id]
if tag_type, tagged := tag_result_type(checker, value.type); tagged && value.kind == .Struct &&
value.active >= 0 {
fields := types.fields_for(&checker.module.types, value.type)
if int(value.active) < len(fields) {
if member, found := find_enum_member(checker, tag_type, symbol.Id(fields[value.active].name)); found {
ct_state_destroy(&state)
return add_hir_expr(checker, hir.Expr{
kind=.Integer, span=expr.span, type=tag_type, integer=i64(member.value),
target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
}
}
}
ct_state_destroy(&state)
value := build_nested_expr(checker, expr.args[0], locals, global_reads, calls, types.INVALID, pkg, file)
if checker.module.exprs[value].kind == .Invalid {
return value
}
tag_type, ok := tag_result_type(checker, checker.module.exprs[value].type)
if !ok {
id := source.add(checker.diagnostics, expr.span, "tag! requires a tagged-union value")
return invalid_hir_expr(checker, expr.span, id)
}
return add_hir_expr(checker, hir.Expr{
kind=.Union_Tag, span=expr.span, type=tag_type, left=value,
target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC,
})
}
build_tagname_intrinsic :: proc(
checker: ^Checker,
expr: ast.Expr,
pkg: ast.Package_Id,
file: ast.File_Id,
) -> hir.Expr_Id {
if len(expr.args) != 1 {
id := source.addf(checker.diagnostics, expr.span, "tagname! expects 1 argument, got %d", len(expr.args))
return invalid_hir_expr(checker, expr.span, id)
}
state := ct_state_make(checker, pkg, file, values=checker.current_comptime_values, diagnose=false)
defer ct_state_destroy(&state)
value_id, flow, ok := ct_eval_expr(&state, expr.args[0], types.INVALID, 0)
if !ok || flow.kind != .Normal || value_id == INVALID_CT_VALUE || int(value_id) >= len(state.values) {
id := source.add(checker.diagnostics, expr.span, "tagname! requires a comptime-known enum value")
return invalid_hir_expr(checker, expr.span, id)
}
value := state.values[value_id]
name, name_ok := enum_member_name_from_value(checker, value.type, value.integer)
if value.kind != .Integer || !name_ok {
id := source.add(checker.diagnostics, expr.span, "tagname! requires a comptime-known enum value")
return invalid_hir_expr(checker, expr.span, id)
}
string_id := intern_comptime_string(checker, name)
return add_hir_expr(checker, hir.Expr{
kind=.String, span=expr.span, type=string_literal_type(checker, string_id), integer=i64(string_id),
target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
is_type_metatype_syntax :: proc(checker: ^Checker, value: ast.Type_Syntax) -> bool {
item, ok := types.node(&checker.module.types, value)
return ok && item.name == u32(checker.type_symbol) && item.qualifier == 0
}
is_comptime_type_param :: proc(checker: ^Checker, param: ast.Param) -> bool {
return param.comptime_value && is_type_metatype_syntax(checker, param.type)
}
function_has_comptime_params :: proc(function: ast.Function) -> bool {
for param in function.params {
if param.comptime_value {
return true
}
}
return false
}
runtime_param_count :: proc(function: ast.Function) -> int {
count := 0
for param in function.params {
if !param.comptime_value {
count += 1
}
}
return count
}
can_fold_zero_runtime_call :: proc(checker: ^Checker, function: ast.Function) -> bool {
return function.has_body && !function.c_abi && runtime_param_count(function) == 0 &&
!types.is_void(function.result) &&
!types.is_constraint(function.result) &&
!is_type_metatype_syntax(checker, function.result)
}
comptime_param_count :: proc(function: ast.Function) -> int {
count := 0
for param in function.params {
if param.comptime_value {
count += 1
}
}
return count
}
fits_signed_type :: proc(value: i128, value_type: types.Type, selected := target.DEFAULT) -> bool {
if !types.is_signed(value_type, selected) {
return false
}
limit := i128(1) << u32(types.bits(value_type, selected) - 1)
return value >= -limit && value < limit
}
fits_unsigned_type :: proc(value: i128, value_type: types.Type, selected := target.DEFAULT) -> bool {
if !types.is_unsigned(value_type, selected) || value < 0 {
return false
}
limit := i128(1) << u32(types.bits(value_type, selected))
return value < limit
}
fits_integer_type :: proc(value: i128, value_type: types.Type, selected := target.DEFAULT) -> bool {
return fits_signed_type(value, value_type, selected) || fits_unsigned_type(value, value_type, selected)
}
fits_i64 :: proc(value: i128) -> bool {
return fits_signed_type(value, types.I64)
}
fits_u64 :: proc(value: i128) -> bool {
return value >= 0 && value <= i128(0xffff_ffff_ffff_ffff)
}
constraint_integer_literal_type :: proc(constraint: types.Type, value: i128) -> types.Type {
if constraint == types.UINT {
return types.smallest_unsigned_for_literal(u64(value)) if fits_u64(value) else types.INVALID
}
return types.smallest_signed_for_literal(i64(value)) if fits_i64(value) else types.INVALID
}
constraint_recovery_type :: proc(checker: ^Checker, constraint: types.Type) -> types.Type {
switch constraint {
case types.UINT: return types.U64
case types.FLOAT: return types.F64
case types.RANGE: return types.range(&checker.module.types, types.I64)
case: return types.I64
}
}
type_from_syntax :: proc(
checker: ^Checker,
value: ast.Type_Syntax,
pkg := ast.Package_Id(0),
file := ast.File_Id(0),
depth := 0,
active_state: ^Ct_State = nil,
) -> types.Type {
if depth > 64 {
return types.INVALID
}
item, ok := types.node(&checker.module.types, value)
if !ok {
return value
}
if item.qualifier == 0 && item.name != 0 {
if actual, ok := current_comptime_type(checker, symbol.Id(item.name)); ok {
return actual
}
}
store := &checker.module.types
changed := false
#partial switch item.kind {
case .Alias:
return type_from_syntax(checker, item.child, pkg, file, depth+1, active_state)
case .Array:
child := type_from_syntax(checker, item.child, pkg, file, depth+1, active_state)
changed = changed || child != item.child
item.child = child
if item.unresolved_count {
expr_id := ast.Expr_Id(item.count_expr)
span := source.Span{}
if expr_id != ast.INVALID_EXPR && int(expr_id) < len(checker.ast_module.exprs) {
span = checker.ast_module.exprs[expr_id].span
}
constant := eval_integer_constant_in_state(active_state, expr_id) if active_state != nil else
eval_integer_constant_in_context(checker, expr_id, pkg, file)
if constant.kind == .Value {
switch {
case constant.value < 0:
source.add(checker.diagnostics, span, "array count must be non-negative")
return types.INVALID
case constant.value > i128(0xffff_ffff_ffff_ffff):
source.add(checker.diagnostics, span, "array count does not fit in u64")
return types.INVALID
case:
item.count = u64(constant.value)
item.unresolved_count = false
item.count_expr = 0
changed = true
}
} else {
if constant.kind == .Integer_Division {
source.add(checker.diagnostics, span, "integer '/' is not allowed; use divtrunc!, divfloor!, divexact!, or divceil!")
return types.INVALID
}
source.add(checker.diagnostics, span, "array count must be a compile-time integer expression")
return types.INVALID
}
}
return types.intern(store, item)
case .Pointer, .Slice, .Optional, .Range, .Fallible:
child := type_from_syntax(checker, item.child, pkg, file, depth+1, active_state)
extra := type_from_syntax(checker, item.extra, pkg, file, depth+1, active_state)
item.child = child
item.extra = extra
return types.intern(store, item)
case .Distinct, .Enum:
child := type_from_syntax(checker, item.child, pkg, file, depth+1, active_state)
extra := type_from_syntax(checker, item.extra, pkg, file, depth+1, active_state)
changed = child != item.child || extra != item.extra
item.child = child
item.extra = extra
case .Function:
params := types.params_for(store, value)
resolved_params := make([]types.Type, len(params), checker.allocator)
defer delete(resolved_params, checker.allocator)
for param, index in params {
resolved_params[index] = type_from_syntax(checker, param.type, pkg, file, depth+1, active_state)
}
result := type_from_syntax(checker, item.child, pkg, file, depth+1, active_state)
return types.function(store, resolved_params, result, item.c_abi, item.variadic)
case .Sum:
left := type_from_syntax(checker, item.child, pkg, file, depth+1, active_state)
right := type_from_syntax(checker, item.extra, pkg, file, depth+1, active_state)
composed, compose_error := types.compose_sum(store, left, right)
if compose_error == .Unsupported {
source.add(checker.diagnostics, source.Span{}, "only native unbacked enums and tagged unions can be composed with '|'")
return types.INVALID
}
if compose_error == .Conflict {
source.add(checker.diagnostics, source.Span{}, "sum composition contains the same variant name with different payload types")
return types.INVALID
}
return composed
case .Type_Call:
expr_id := ast.Expr_Id(item.count_expr)
call_file := file
if expr_id != ast.INVALID_EXPR && int(expr_id) < len(checker.ast_module.exprs) {
call_file = ast.File_Id(checker.ast_module.exprs[expr_id].span.file)
}
return resolve_type_factory_call(checker, expr_id, pkg, call_file)
}
if changed {
return types.intern(store, item)
}
return value
}
function_channel_type :: proc(checker: ^Checker, function: ast.Function) -> types.Type {
result := type_from_syntax(checker, function.result, function.pkg, function.file)
if function.pkg == 0 && function.name == checker.main_symbol && result == types.INT {
result = types.I32
}
if types.is_valid(function.error) {
return types.fallible(&checker.module.types, result, type_from_syntax(checker, function.error, function.pkg, function.file))
}
return result
}
is_runtime_type :: proc(checker: ^Checker, value: types.Type) -> bool {
return types.is_runtime_value(value, &checker.module.types)
}
type_contains_unresolved_named :: proc(checker: ^Checker, value: types.Type, depth := 0) -> bool {
if depth > 64 {
return true
}
item, ok := types.node(&checker.module.types, value)
if !ok {
return false
}
if item.kind == .Named && !item.declared {
return true
}
if type_contains_unresolved_named(checker, item.child, depth+1) ||
type_contains_unresolved_named(checker, item.extra, depth+1) {
return true
}
if item.kind == .Function {
for param in types.params_for(&checker.module.types, value) {
if type_contains_unresolved_named(checker, param.type, depth+1) {
return true
}
}
}
return false
}
is_comptime_value_type :: proc(checker: ^Checker, value: types.Type, depth := 0) -> bool {
if depth > 256 || !types.is_valid(value) {
return false
}
if is_type_metatype_syntax(checker, value) {
return true
}
if is_runtime_type(checker, value) {
return true
}
item, ok := types.node(&checker.module.types, value)
if !ok {
return false
}
if item.kind == .Function {
return true
}
if item.kind == .Array || item.kind == .Optional || item.kind == .Alias || item.kind == .Distinct {
return is_comptime_value_type(checker, item.child, depth+1)
}
if item.kind == .Struct || item.kind == .Union {
if !item.declared || item.opaque || item.c_layout ||
(item.kind == .Union && !types.is_tagged_union(value, &checker.module.types)) {
return false
}
for field in types.fields_for(&checker.module.types, value) {
if !types.is_void(field.type) && !is_comptime_value_type(checker, field.type, depth+1) {
return false
}
}
return true
}
return false
}
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_typed_integer_fold_candidate :: proc(checker: ^Checker, expr_id: ast.Expr_Id, depth := 0) -> bool {
if depth > 64 || 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 .Integer:
return true
case .Negate, .Bit_Not, .Cast:
return is_typed_integer_fold_candidate(checker, expr.left, depth+1)
case .Add, .Sub, .Mul, .Bit_And, .Bit_Or, .Bit_Xor, .Shift_Left, .Shift_Right, .Shift_Left_Saturating:
return is_typed_integer_fold_candidate(checker, expr.left, depth+1) &&
is_typed_integer_fold_candidate(checker, expr.right, depth+1)
}
return false
}
is_numeric_demand :: proc(value: types.Type, selected := target.DEFAULT) -> bool {
return types.is_concrete_scalar(value) && !types.is_bool(value) ||
types.is_float(value, selected)
}
string_literal_type :: proc(checker: ^Checker, string_id: u64) -> types.Type {
length: u64
if string_id < u64(len(checker.ast_module.strings)) {
length = u64(len(checker.ast_module.strings[string_id]))
}
array := types.array(&checker.module.types, types.U8, length, false, true, 0)
return types.pointer(&checker.module.types, array, false, false)
}
container_pointer_type :: proc(store: ^types.Store, item: types.Node) -> types.Type {
return types.pointer(store, item.child, item.mutable, true, item.has_sentinel, item.sentinel)
}
resolve_inferred_array :: proc(checker: ^Checker, value: types.Type, expr_id: ast.Expr_Id) -> types.Type {
item, ok := types.node(&checker.module.types, value)
if !ok || item.kind != .Array || !item.inferred_count ||
expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) {
return value
}
expr := checker.ast_module.exprs[expr_id]
if expr.kind != .Array {
return value
}
return types.with_array_count(&checker.module.types, value, u64(len(expr.args)))
}
has_inferred_array_count :: proc(checker: ^Checker, value: types.Type) -> bool {
item, ok := types.node(&checker.module.types, value)
return ok && item.kind == .Array && item.inferred_count
}
resolve_inferred_array_from_type :: proc(checker: ^Checker, value, inferred: types.Type) -> types.Type {
item, ok := types.node(&checker.module.types, value)
actual, actual_ok := types.node(&checker.module.types, inferred)
if !ok || !actual_ok || item.kind != .Array || actual.kind != .Array || !item.inferred_count {
return value
}
if item.child != actual.child || item.mutable != actual.mutable ||
item.has_sentinel != actual.has_sentinel ||
(item.has_sentinel && item.sentinel != actual.sentinel) {
return value
}
return types.with_array_count(&checker.module.types, value, actual.count)
}
function_index_less :: proc(left, right: Function_Index_Entry) -> bool {
if left.scope != right.scope {
return left.scope < right.scope
}
if left.name != right.name {
return int(left.name) < int(right.name)
}
return left.id < right.id
}
global_index_less :: proc(left, right: Global_Index_Entry) -> bool {
if left.scope != right.scope {
return left.scope < right.scope
}
if left.name != right.name {
return int(left.name) < int(right.name)
}
return left.id < right.id
}
import_index_less :: proc(left, right: Import_Index_Entry) -> bool {
if left.scope != right.scope {
return left.scope < right.scope
}
if left.name != right.name {
return int(left.name) < int(right.name)
}
return left.id < right.id
}
find_function_symbol :: proc(index: []Function_Index_Entry, scope: ast.Package_Id, name: symbol.Id, file := ast.INVALID_FILE) -> ast.Function_Id {
low := 0
high := len(index)
for low < high {
middle := low + (high-low)/2
entry := index[middle]
if entry.scope < scope || entry.scope == scope && int(entry.name) < int(name) {
low = middle + 1
} else {
high = middle
}
}
visible := ast.INVALID_FUNCTION
for low < len(index) && index[low].scope == scope && index[low].name == name {
entry := index[low]
if entry.hidden {
if file != ast.INVALID_FILE {
return entry.id
}
} else {
visible = entry.id
}
low += 1
}
return visible
}
find_global_symbol :: proc(index: []Global_Index_Entry, scope: ast.Package_Id, name: symbol.Id, file := ast.INVALID_FILE) -> ast.Global_Id {
low := 0
high := len(index)
for low < high {
middle := low + (high-low)/2
entry := index[middle]
if entry.scope < scope || entry.scope == scope && int(entry.name) < int(name) {
low = middle + 1
} else {
high = middle
}
}
visible := ast.INVALID_GLOBAL
for low < len(index) && index[low].scope == scope && index[low].name == name {
entry := index[low]
if entry.hidden {
if file != ast.INVALID_FILE {
return entry.id
}
} else {
visible = entry.id
}
low += 1
}
return visible
}
find_import_symbol :: proc(index: []Import_Index_Entry, scope: ast.File_Id, name: symbol.Id) -> ast.Import_Id {
low := 0
high := len(index)
for low < high {
middle := low + (high-low)/2
entry := index[middle]
if entry.scope < scope || entry.scope == scope && int(entry.name) < int(name) {
low = middle + 1
} else {
high = middle
}
}
if low < len(index) && index[low].scope == scope && index[low].name == name {
return index[low].id
}
return ast.INVALID_IMPORT
}
build_symbol_indexes :: proc(checker: ^Checker) {
function_count := 0
for function in checker.ast_module.functions {
if !function.generated {
function_count += 1
}
}
for alias in checker.ast_module.aliases {
if alias.valid && alias.kind == .Function {
function_count += 1
}
}
checker.function_index = make([]Function_Index_Entry, function_count, checker.allocator)
function_index := 0
for function, id in checker.ast_module.functions {
if function.generated {
continue
}
checker.function_index[function_index] = Function_Index_Entry{scope=function.pkg, hidden=function.package_hidden, name=function.name, id=ast.function_id(id)}
function_index += 1
}
for alias in checker.ast_module.aliases {
if alias.valid && alias.kind == .Function {
checker.function_index[function_index] = Function_Index_Entry{scope=alias.pkg, hidden=alias.package_hidden, name=alias.name, id=ast.Function_Id(alias.target)}
function_index += 1
}
}
slice.sort_by(checker.function_index, function_index_less)
global_count := len(checker.ast_module.globals)
for alias in checker.ast_module.aliases {
if alias.valid && alias.kind == .Global {
global_count += 1
}
}
checker.global_index = make([]Global_Index_Entry, global_count, checker.allocator)
for global, id in checker.ast_module.globals {
checker.global_index[id] = Global_Index_Entry{scope=global.pkg, hidden=global.package_hidden, name=global.name, id=ast.global_id(id)}
}
global_index := len(checker.ast_module.globals)
for alias in checker.ast_module.aliases {
if alias.valid && alias.kind == .Global {
checker.global_index[global_index] = Global_Index_Entry{scope=alias.pkg, hidden=alias.package_hidden, name=alias.name, id=ast.Global_Id(alias.target)}
global_index += 1
}
}
slice.sort_by(checker.global_index, global_index_less)
import_count := 0
for import_item in checker.ast_module.imports {
if !import_item.test_only {
import_count += 1
}
}
checker.import_index = make([]Import_Index_Entry, import_count, checker.allocator)
import_index := 0
for import_item, id in checker.ast_module.imports {
if import_item.test_only {
continue
}
checker.import_index[import_index] = Import_Index_Entry{scope=import_item.file, name=import_item.alias, id=ast.import_id(id)}
import_index += 1
}
slice.sort_by(checker.import_index, import_index_less)
}
find_template :: proc(checker: ^Checker, name: symbol.Id, pkg := ast.Package_Id(0), file := ast.INVALID_FILE) -> ast.Function_Id {
return find_function_symbol(checker.function_index, pkg, name, file)
}
configure_entry_point :: proc(checker: ^Checker) {
main_template := find_template(checker, checker.main_symbol, 0)
if main_template == ast.INVALID_FUNCTION {
return
}
main := checker.ast_module.functions[main_template]
if len(main.params) == 0 {
checker.entry_point = .Plain
return
}
if len(main.params) != 1 || main.params[0].comptime_value {
return
}
parameter_type := types.resolve_alias(
type_from_syntax(checker, main.params[0].type, main.pkg, main.file),
&checker.module.types,
)
init_name := symbol.intern(checker.symbols, "Init")
process_package := ast.INVALID_PACKAGE
for import_item in checker.ast_module.imports {
if import_item.valid && import_item.path == "@std/process" {
candidate := types.find_named(&checker.module.types, u32(import_item.target), u32(init_name))
if types.equal(parameter_type, candidate) {
process_package = import_item.target
break
}
}
}
if process_package == ast.INVALID_PACKAGE {
return
}
io_name := symbol.intern(checker.symbols, "Io")
io_package := ast.INVALID_PACKAGE
io_type := types.INVALID
for import_item in checker.ast_module.imports {
if import_item.valid && import_item.path == "@std/io" {
candidate := types.find_named(&checker.module.types, u32(import_item.target), u32(io_name))
io_package = import_item.target
io_type = candidate
break
}
}
if io_package == ast.INVALID_PACKAGE {
return
}
init_item, init_ok := types.node(&checker.module.types, parameter_type)
init_fields := types.fields_for(&checker.module.types, parameter_type)
io_field_name := symbol.intern(checker.symbols, "io")
if !init_ok || init_item.kind != .Struct || init_item.tuple || init_item.c_layout ||
len(init_fields) != 1 || init_fields[0].name != u32(io_field_name) ||
!types.equal(types.resolve_alias(init_fields[0].type, &checker.module.types), io_type) {
checker.template_diagnostics[main_template] = source.add(
checker.diagnostics,
main.span,
"@std/process Init must be an auto-layout record containing exactly 'io io.Io'",
)
return
}
provider_name := symbol.intern(checker.symbols, "system")
provider := ast.INVALID_FUNCTION
provider_count := 0
for function, function_id in checker.ast_module.functions {
if function.pkg != io_package || function.name != provider_name {
continue
}
provider_count += 1
result := types.resolve_alias(
type_from_syntax(checker, function.result, function.pkg, function.file),
&checker.module.types,
)
if function.package_hidden && function.has_body && !function.c_abi && len(function.params) == 0 &&
!types.is_valid(function.error) && types.equal(result, io_type) {
provider = ast.function_id(function_id)
}
}
if provider_count != 1 || provider == ast.INVALID_FUNCTION {
checker.template_diagnostics[main_template] = source.add(
checker.diagnostics,
main.span,
"@std/io does not provide the required 'hide system func() Io' startup implementation",
)
return
}
checker.entry_point = .Process
checker.io_provider_template = provider
}
find_global :: proc(checker: ^Checker, name: symbol.Id, pkg := ast.Package_Id(0), file := ast.INVALID_FILE) -> ast.Global_Id {
return find_global_symbol(checker.global_index, pkg, name, file)
}
find_import :: proc(checker: ^Checker, file: ast.File_Id, alias: symbol.Id, mark_used := false) -> ast.Import_Id {
id := find_import_symbol(checker.import_index, file, alias)
if id != ast.INVALID_IMPORT && mark_used {
checker.ast_module.imports[id].used = true
}
return id
}
declared_type_named :: proc(checker: ^Checker, pkg: ast.Package_Id, name: symbol.Id, file := ast.INVALID_FILE) -> bool {
id := types.find_named(&checker.module.types, u32(pkg), u32(name), file=u32(file))
item, ok := types.node(&checker.module.types, id)
return ok && item.declared
}
type_declaration_conflicts :: proc(checker: ^Checker, pkg: ast.Package_Id, name: symbol.Id) -> bool {
for item in checker.module.types.nodes {
if item.declared && item.pkg == u32(pkg) && item.name == u32(name) {
return true
}
}
return false
}
visible_name_kind :: proc(
checker: ^Checker,
name: symbol.Id,
pkg: ast.Package_Id,
file: ast.File_Id,
locals: []Build_Local = nil,
labels: []symbol.Id = nil,
yield_targets: []Yield_Target = nil,
) -> string {
if !symbol.is_valid(name) || name == checker.sink_symbol {
return ""
}
if _, ok := current_comptime_value(checker, name); ok {
return "comptime parameter"
}
if _, ok := find_build_local(locals, name); ok {
return "local"
}
for label in labels {
if label == name {
return "label"
}
}
for target in yield_targets {
if target.label == name {
return "label"
}
}
if find_import(checker, file, name) != ast.INVALID_IMPORT {
return "import"
}
if find_global(checker, name, pkg, file) != ast.INVALID_GLOBAL {
return "global"
}
if find_template(checker, name, pkg, file) != ast.INVALID_FUNCTION {
return "function"
}
if declared_type_named(checker, pkg, name, file) {
return "type"
}
return ""
}
add_shadow_diagnostic :: proc(
checker: ^Checker,
span: source.Span,
name: symbol.Id,
decl_kind: string,
pkg: ast.Package_Id,
file: ast.File_Id,
locals: []Build_Local = nil,
labels: []symbol.Id = nil,
yield_targets: []Yield_Target = nil,
) -> source.Diagnostic_Id {
kind := visible_name_kind(checker, name, pkg, file, locals, labels, yield_targets)
if len(kind) == 0 {
return source.INVALID_DIAGNOSTIC
}
return source.addf(
checker.diagnostics,
span,
"%s '%s' shadows visible %s",
decl_kind,
symbol_text(checker, name),
kind,
)
}
add_label_shadow_diagnostic :: proc(ctx: ^Build_Ctx, span: source.Span, label: symbol.Id) -> source.Diagnostic_Id {
if !symbol.is_valid(label) {
return source.INVALID_DIAGNOSTIC
}
yield_targets := ctx.yield_targets^[:]
if len(yield_targets) > 0 && yield_targets[len(yield_targets) - 1].label == label {
label_is_active_loop := false
for loop_label in ctx.loop_labels^[:] {
if loop_label == label {
label_is_active_loop = true
break
}
}
if !label_is_active_loop {
yield_targets = yield_targets[:len(yield_targets) - 1]
}
}
return add_shadow_diagnostic(
ctx.checker, span, label, "label",
ctx.pkg, ctx.file, ctx.locals^[:], ctx.loop_labels^[:], yield_targets,
)
}
expr_package :: proc(checker: ^Checker, expr: ast.Expr, pkg: ast.Package_Id, file: ast.File_Id, mark_used := false) -> (ast.Package_Id, bool) {
if !symbol.is_valid(expr.qualifier) {
return pkg, true
}
import_id := find_import(checker, file, expr.qualifier, mark_used)
if import_id == ast.INVALID_IMPORT {
return ast.INVALID_PACKAGE, false
}
import_item := checker.ast_module.imports[import_id]
if import_item.target == ast.INVALID_PACKAGE || int(import_item.target) >= len(checker.ast_module.packages) ||
!checker.ast_module.packages[import_item.target].available {
return import_item.target, false
}
return import_item.target, true
}
expr_lookup_file :: proc(expr: ast.Expr, file: ast.File_Id) -> ast.File_Id {
return ast.INVALID_FILE if symbol.is_valid(expr.qualifier) else file
}
expr_symbol_span :: proc(checker: ^Checker, expr: ast.Expr, name: symbol.Id) -> source.Span {
span := expr.span
span.end = span.start+source.Offset(len(symbol_text(checker, name)))
return span
}
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 {
span := expr_symbol_span(checker, expr, expr.qualifier)
id := source.addf(checker.diagnostics, span, "unknown symbol '%s'", symbol_text(checker, expr.qualifier))
source.set_primary_label(checker.diagnostics, id, "unknown symbol")
return id
}
return source.addf(checker.diagnostics, expr.span, "unavailable imported package '%s'", symbol_text(checker, expr.qualifier))
}
add_name_resolution_diagnostic :: proc(checker: ^Checker, expr: ast.Expr, target_pkg: ast.Package_Id, file: ast.File_Id) -> source.Diagnostic_Id {
if find_template(checker, expr.name, target_pkg, expr_lookup_file(expr, file)) != ast.INVALID_FUNCTION {
template := find_template(checker, expr.name, target_pkg, expr_lookup_file(expr, file))
id := source.addf(checker.diagnostics, expr_symbol_span(checker, expr, expr.name), "'%s' is a function, not a global value", symbol_text(checker, expr.name))
if !checker.ast_module.functions[template].imported {
source.add_secondary_label(checker.diagnostics, id, checker.ast_module.functions[template].span, "function declared here")
}
return id
}
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),
)
}
span := expr_symbol_span(checker, expr, expr.name)
id := source.addf(checker.diagnostics, span, "unknown symbol '%s'", symbol_text(checker, expr.name))
source.set_primary_label(checker.diagnostics, id, "unknown symbol")
return id
}
add_call_resolution_diagnostic :: proc(checker: ^Checker, expr: ast.Expr, target_pkg: ast.Package_Id, file: ast.File_Id) -> source.Diagnostic_Id {
if find_global(checker, expr.name, target_pkg, expr_lookup_file(expr, file)) != ast.INVALID_GLOBAL {
global := find_global(checker, expr.name, target_pkg, expr_lookup_file(expr, file))
id := source.addf(checker.diagnostics, expr_symbol_span(checker, expr, expr.name), "'%s' is a global, not a function", symbol_text(checker, expr.name))
if !checker.ast_module.globals[global].external {
source.add_secondary_label(checker.diagnostics, id, checker.ast_module.globals[global].span, "global declared here")
}
return id
}
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),
)
}
span := expr_symbol_span(checker, expr, expr.name)
id := source.addf(checker.diagnostics, span, "unknown symbol '%s'", symbol_text(checker, expr.name))
source.set_primary_label(checker.diagnostics, id, "unknown symbol")
return id
}
find_unsupported :: proc(checker: ^Checker, pkg: ast.Package_Id, name: symbol.Id) -> (ast.Unsupported, bool) {
for item in checker.ast_module.unsupported {
if item.pkg == pkg && item.name == name {
return item, true
}
}
return {}, false
}
add_unsupported_diagnostic :: proc(checker: ^Checker, span: source.Span, pkg: ast.Package_Id, name: symbol.Id) -> source.Diagnostic_Id {
if item, ok := find_unsupported(checker, pkg, name); ok {
return source.addf(
checker.diagnostics,
span,
"C declaration '%s' is unavailable: %s",
symbol_text(checker, name),
item.reason,
)
}
return source.INVALID_DIAGNOSTIC
}
add_unsupported_type_diagnostic :: proc(
checker: ^Checker,
span: source.Span,
value: types.Type,
depth := 0,
) -> source.Diagnostic_Id {
if depth > 64 {
return source.INVALID_DIAGNOSTIC
}
item, ok := types.node(&checker.module.types, value)
if !ok {
return source.INVALID_DIAGNOSTIC
}
if item.kind == .Alias {
return add_unsupported_diagnostic(checker, span, ast.Package_Id(item.pkg), symbol.Id(item.name))
}
if types.is_valid(item.child) {
return add_unsupported_type_diagnostic(checker, span, item.child, depth+1)
}
if types.is_valid(item.extra) {
return add_unsupported_type_diagnostic(checker, span, item.extra, depth+1)
}
return source.INVALID_DIAGNOSTIC
}
function_signatures_equal :: proc(left, right: ast.Function) -> bool {
if left.result != right.result || left.error != right.error ||
left.variadic != right.variadic || len(left.params) != len(right.params) {
return false
}
for param, index in left.params {
if param.type != right.params[index].type ||
param.comptime_value != right.params[index].comptime_value {
return false
}
}
return true
}
valid_call_arity :: proc(function: ast.Function, count: int) -> bool {
return count >= len(function.params) if function.variadic else count == len(function.params)
}
Call_Argument_Mode :: enum u8 {
Invalid,
Explicit,
Inferred,
}
call_param_index :: proc(mapping: []int, source_index: int) -> int {
if source_index < 0 || source_index >= len(mapping) {
return -1
}
return mapping[source_index]
}
next_runtime_call_arg :: proc(function: ast.Function, mapping: []int, start, source_count: int) -> int {
index := start
for index < source_count {
param_index := call_param_index(mapping, index)
if param_index >= len(function.params) || !function.params[param_index].comptime_value {
break
}
index += 1
}
return index
}
call_mapping_mode :: proc(function: ast.Function, mapping: []int) -> Call_Argument_Mode {
if len(mapping) != len(function.params) {
return .Inferred
}
for value, index in mapping {
if value != index {
return .Inferred
}
}
return .Explicit
}
comptime_binding_index :: proc(function: ast.Function, prefix: int, name: symbol.Id) -> (int, bool) {
ordinal := 0
for param in function.params {
if !param.comptime_value {
continue
}
if ordinal >= prefix {
return -1, false
}
if param.name == name {
return ordinal, true
}
ordinal += 1
}
return -1, false
}
comptime_param_for_name :: proc(function: ast.Function, name: symbol.Id) -> (ast.Param, bool) {
for param in function.params {
if param.comptime_value && param.name == name {
return param, true
}
}
return {}, false
}
Call_Mapping_Search :: struct {
checker: ^Checker,
function: ^ast.Function,
args: []ast.Expr_Id,
current: []int,
candidates: ^[dynamic][]int,
}
is_comptime_string_param :: proc(checker: ^Checker, param: ast.Param, function: ast.Function) -> bool {
declared := types.resolve_alias(type_from_syntax(checker, param.type, function.pkg, function.file), &checker.module.types)
item, ok := types.container(declared, &checker.module.types)
return ok && item.kind == .Slice && !item.mutable && item.child == types.U8
}
explicit_comptime_argument_valid :: proc(
checker: ^Checker,
function: ast.Function,
param: ast.Param,
arg: ast.Expr_Id,
pkg: ast.Package_Id,
file: ast.File_Id,
) -> bool {
if arg == ast.INVALID_EXPR || int(arg) >= len(checker.ast_module.exprs) {
return false
}
expr := checker.ast_module.exprs[arg]
if expr.kind == .Inference_Hole {
return true
}
if is_comptime_type_param(checker, param) {
// Avoid asking the ordinary type resolver to diagnose while candidates are
// being probed. A call can be a type argument only when its declaration is
// a type factory.
if expr.kind == .Call {
target_pkg, available := expr_package(checker, expr, pkg, file, false)
if !available {
return false
}
template := find_template(checker, expr.name, target_pkg, expr_lookup_file(expr, file))
if template == ast.INVALID_FUNCTION ||
!is_type_metatype_syntax(checker, checker.ast_module.functions[template].result) {
return false
}
}
_, ok := resolve_type_argument(checker, arg, pkg, file)
return ok
}
if type_pattern_mentions_comptime(checker, function, comptime_param_count(function), param.type) {
return true
}
if is_comptime_string_param(checker, param, function) {
_, ok := comptime_string_argument(checker, arg, pkg, file)
return ok
}
if types.is_concrete_integer(type_from_syntax(checker, param.type, function.pkg, function.file)) {
return eval_integer_constant_in_context(checker, arg, pkg, file).kind == .Value
}
declared := type_from_syntax(checker, param.type, function.pkg, function.file)
_, ok := eval_static_comptime_value(checker, param.name, arg, declared, pkg, file)
return ok
}
search_call_mappings :: proc(search: ^Call_Mapping_Search, param_index, source_index: int) {
if len(search.candidates^) >= COMPTIME_EVAL_QUOTA {
return
}
function := search.function^
if param_index == len(function.params) {
if source_index != len(search.args) {
return
}
candidate := make([]int, len(search.current), search.checker.allocator)
copy(candidate, search.current)
append(search.candidates, candidate)
return
}
param := function.params[param_index]
if param.comptime_value {
if source_index < len(search.args) {
search.current[source_index] = param_index
search_call_mappings(search, param_index+1, source_index+1)
search.current[source_index] = -1
}
search_call_mappings(search, param_index+1, source_index)
return
}
if source_index >= len(search.args) {
return
}
search.current[source_index] = param_index
search_call_mappings(search, param_index+1, source_index+1)
search.current[source_index] = -1
}
call_mapping_semantically_valid :: proc(
checker: ^Checker,
function: ast.Function,
mapping: []int,
args: []ast.Expr_Id,
expected: types.Type,
locals: []Infer_Local,
pkg: ast.Package_Id,
file: ast.File_Id,
) -> (bool, string) {
actual_args := make([]types.Type, len(function.params), checker.allocator)
defer delete(actual_args, checker.allocator)
for source_index in 0..<len(args) {
param_index := call_param_index(mapping, source_index)
if param_index < 0 || param_index >= len(function.params) {
continue
}
if function.params[param_index].comptime_value {
if !explicit_comptime_argument_valid(
checker, function, function.params[param_index], args[source_index], pkg, file,
) {
return false, fmt.aprintf(
"argument %d is not a valid comptime value for parameter '%s'",
source_index+1, symbol_text(checker, function.params[param_index].name),
allocator=checker.allocator,
)
}
continue
}
actual_args[param_index] = infer_expr(checker, args[source_index], locals, pkg, file)
}
inference_failure := ""
values, ok := infer_call_comptime_values(
checker, function, comptime_param_count(function), mapping, args, actual_args,
expected, pkg, file, diagnose=false, failure=&inference_failure,
)
defer delete(values, checker.allocator)
if !ok {
if len(inference_failure) > 0 {
return false, inference_failure
}
return false, fmt.aprintf("could not infer or evaluate every comptime parameter", allocator=checker.allocator)
}
delete(inference_failure, checker.allocator)
previous := checker.current_comptime_values
checker.current_comptime_values = values
defer checker.current_comptime_values = previous
for param, index in function.params {
if param.comptime_value {
continue
}
actual := actual_args[index]
declared := type_from_syntax(checker, param.type, function.pkg, function.file)
source_index := -1
for mapped_param, candidate_source in mapping {
if mapped_param == index {
source_index = candidate_source
break
}
}
if !is_runtime_type(checker, actual) && !can_implicitly_convert_type(checker, actual, declared) {
return false, fmt.aprintf(
"argument %d is not a runtime value", source_index+1,
allocator=checker.allocator,
)
}
if types.is_constraint(declared) {
if !types.is_valid(types.constraint_target(declared, actual, &checker.module.types)) {
return false, fmt.aprintf(
"argument %d of type %s does not satisfy %s",
source_index+1, type_label(checker, actual), type_label(checker, declared),
allocator=checker.allocator,
)
}
} else if !can_implicitly_convert_type(checker, actual, declared) {
if source_index < 0 ||
!is_numeric_constant_expr(checker, args[source_index]) ||
!expr_accepts_numeric_demand(checker, args[source_index], declared, locals, pkg, file) {
return false, fmt.aprintf(
"argument %d of type %s cannot convert to %s",
source_index+1, type_label(checker, actual), type_label(checker, declared),
allocator=checker.allocator,
)
}
}
}
if is_runtime_type(checker, expected) {
result := function_channel_type(checker, function)
if !types.is_noreturn(result) && !is_runtime_type(checker, result) ||
!can_implicitly_convert_type(checker, result, expected) {
return false, fmt.aprintf(
"result type %s cannot convert to expected type %s",
type_label(checker, result), type_label(checker, expected),
allocator=checker.allocator,
)
}
}
return true, ""
}
call_argument_mapping :: proc(
checker: ^Checker,
function: ^ast.Function,
args: []ast.Expr_Id,
pkg: ast.Package_Id,
file: ast.File_Id,
expected := types.INVALID,
locals: []Infer_Local = nil,
) -> (mapping: []int, mode: Call_Argument_Mode, comptime_count: int, failure: string) {
if function.variadic || function.c_abi {
if !valid_call_arity(function^, len(args)) {
return nil, .Invalid, 0, ""
}
mapping = make([]int, len(args), checker.allocator)
for &value, index in mapping {
value = index
}
return mapping, .Explicit, 0, ""
}
current := make([]int, len(args), checker.allocator)
defer delete(current, checker.allocator)
for &value in current {
value = -1
}
candidates: [dynamic][]int
candidates.allocator = checker.allocator
defer {
for candidate in candidates {
delete(candidate, checker.allocator)
}
delete(candidates)
}
search := Call_Mapping_Search{
checker=checker,
function=function,
args=args,
current=current,
candidates=&candidates,
}
search_call_mappings(&search, 0, 0)
selected_index := -1
failures: [dynamic]string
failures.allocator = checker.allocator
defer {
for item in failures {
delete(item, checker.allocator)
}
delete(failures)
}
if len(candidates) == 1 {
selected_index = 0
}
if selected_index < 0 && len(candidates) > 1 {
for candidate, index in candidates {
valid, reason := call_mapping_semantically_valid(
checker, function^, candidate, args, expected, locals, pkg, file,
)
if valid {
if selected_index >= 0 {
return nil, .Invalid, comptime_param_count(function^), fmt.aprintf(
"multiple complete argument mappings satisfy the call",
allocator=checker.allocator,
)
}
selected_index = index
} else {
append(&failures, reason)
}
}
}
if selected_index < 0 {
// Prefer the mapping that treats an array literal as a dependent aggregate.
// It can then report the contextual element error instead of an ambiguous
// hidden-comptime-parameter mapping failure.
dependent_array_candidate := -1
ambiguous := false
for candidate, index in candidates {
matches := false
for arg_id, source_index in args {
param_index := call_param_index(candidate, source_index)
if param_index < 0 || param_index >= len(function.params) ||
!function.params[param_index].comptime_value ||
arg_id == ast.INVALID_EXPR || int(arg_id) >= len(checker.ast_module.exprs) ||
checker.ast_module.exprs[arg_id].kind != .Array {
continue
}
matches = matches || type_pattern_mentions_comptime(
checker, function^, comptime_param_count(function^), function.params[param_index].type,
)
}
if matches {
if dependent_array_candidate >= 0 {
ambiguous = true
} else {
dependent_array_candidate = index
}
}
}
if dependent_array_candidate >= 0 && !ambiguous {
selected_index = dependent_array_candidate
}
}
if selected_index < 0 {
if len(failures) > 0 {
builder := strings.builder_make(checker.allocator)
defer strings.builder_destroy(&builder)
for reason, index in failures {
if index > 0 {
strings.write_string(&builder, "; ")
}
fmt.sbprintf(&builder, "candidate %d: %s", index+1, reason)
}
return nil, .Invalid, comptime_param_count(function^), fmt.aprintf(
"%s", strings.to_string(builder), allocator=checker.allocator,
)
}
return nil, .Invalid, comptime_param_count(function^), ""
}
selected := make([]int, len(args), checker.allocator)
copy(selected, candidates[selected_index])
identity := len(args) == len(function.params)
if identity {
for value, index in selected {
if value != index {
identity = false
break
}
}
}
return selected, .Explicit if identity else .Inferred, comptime_param_count(function^), ""
}
inferred_comptime_value_equal :: proc(checker: ^Checker, left, right: Comptime_Value) -> bool {
if left.kind != right.kind {
return false
}
if left.kind == .Type {
return types.equal(
types.resolve_alias(left.type, &checker.module.types),
types.resolve_alias(right.type, &checker.module.types),
)
}
if !types.equal(left.type, right.type) {
return false
}
if left.kind == .Static {
return left.fingerprint == right.fingerprint && left.key == right.key
}
if left.kind == .String {
return left.text == right.text
}
return left.value == right.value
}
bind_inferred_comptime :: proc(
checker: ^Checker,
function: ast.Function,
prefix: int,
values: []Comptime_Value,
bound: []bool,
name: symbol.Id,
candidate: Comptime_Value,
span: source.Span,
diagnose: bool,
) -> bool {
index, ok := comptime_binding_index(function, prefix, name)
if !ok {
return false
}
value := candidate
value.name = name
if !bound[index] {
values[index] = value
bound[index] = true
return true
}
existing := values[index]
matches := inferred_comptime_value_equal(checker, existing, value)
if !matches && diagnose {
left := type_label(checker, existing.type) if existing.kind == .Type else fmt.aprintf("<comptime value>", allocator=checker.allocator) if existing.kind == .Static else fmt.aprintf("%d", existing.value, allocator=checker.allocator)
right := type_label(checker, value.type) if value.kind == .Type else fmt.aprintf("<comptime value>", allocator=checker.allocator) if value.kind == .Static else fmt.aprintf("%d", value.value, allocator=checker.allocator)
source.addf(checker.diagnostics, span, "conflicting inference for comptime parameter '%s': %s and %s", symbol_text(checker, name), left, right)
if existing.kind != .Type {
delete(left, checker.allocator)
}
if value.kind != .Type {
delete(right, checker.allocator)
}
}
return matches
}
type_pattern_mentions_comptime :: proc(
checker: ^Checker,
function: ast.Function,
prefix: int,
pattern: types.Type,
depth := 0,
type_params_only := false,
) -> bool {
if depth > 64 {
return false
}
item, ok := types.node(&checker.module.types, pattern)
if !ok {
return false
}
if item.qualifier == 0 && item.name != 0 {
if _, found := comptime_binding_index(function, prefix, symbol.Id(item.name)); found {
if !type_params_only {
return true
}
param, param_ok := comptime_param_for_name(function, symbol.Id(item.name))
if param_ok && is_comptime_type_param(checker, param) {
return true
}
}
}
if item.kind == .Array && item.unresolved_count {
expr_id := ast.Expr_Id(item.count_expr)
if expr_id != ast.INVALID_EXPR && int(expr_id) < len(checker.ast_module.exprs) {
expr := checker.ast_module.exprs[expr_id]
if expr.kind == .Name && !symbol.is_valid(expr.qualifier) {
if _, found := comptime_binding_index(function, prefix, expr.name); found {
if !type_params_only {
return true
}
param, param_ok := comptime_param_for_name(function, expr.name)
if param_ok && is_comptime_type_param(checker, param) {
return true
}
}
}
}
}
if item.kind == .Type_Call {
expr_id := ast.Expr_Id(item.count_expr)
if expr_id != ast.INVALID_EXPR && int(expr_id) < len(checker.ast_module.exprs) {
for arg_id in checker.ast_module.exprs[expr_id].args {
if arg_id == ast.INVALID_EXPR || int(arg_id) >= len(checker.ast_module.exprs) {
continue
}
arg := checker.ast_module.exprs[arg_id]
if arg.kind == .Name && !symbol.is_valid(arg.qualifier) {
if _, found := comptime_binding_index(function, prefix, arg.name); found {
if !type_params_only {
return true
}
param, param_ok := comptime_param_for_name(function, arg.name)
if param_ok && is_comptime_type_param(checker, param) {
return true
}
}
}
}
}
}
if types.is_valid(item.child) && type_pattern_mentions_comptime(checker, function, prefix, item.child, depth+1, type_params_only) {
return true
}
if types.is_valid(item.extra) && type_pattern_mentions_comptime(checker, function, prefix, item.extra, depth+1, type_params_only) {
return true
}
if item.kind == .Function {
for field in types.params_for(&checker.module.types, pattern) {
if type_pattern_mentions_comptime(checker, function, prefix, field.type, depth+1, type_params_only) {
return true
}
}
}
return false
}
type_pattern_contains_type_call :: proc(checker: ^Checker, pattern: types.Type, depth := 0) -> bool {
if depth > 64 {
return false
}
item, ok := types.node(&checker.module.types, pattern)
if !ok {
return false
}
if item.kind == .Type_Call {
return true
}
if types.is_valid(item.child) && type_pattern_contains_type_call(checker, item.child, depth+1) {
return true
}
if types.is_valid(item.extra) && type_pattern_contains_type_call(checker, item.extra, depth+1) {
return true
}
if item.kind == .Function {
for field in types.params_for(&checker.module.types, pattern) {
if type_pattern_contains_type_call(checker, field.type, depth+1) {
return true
}
}
}
return false
}
match_inferred_type_pattern :: proc(
checker: ^Checker,
function: ast.Function,
prefix: int,
pattern, actual: types.Type,
values: []Comptime_Value,
bound: []bool,
span: source.Span,
diagnose: bool,
depth := 0,
) -> bool {
if depth > 64 || !types.is_valid(actual) {
return false
}
store := &checker.module.types
actual_type := types.resolve_alias(actual, store)
pattern_item, pattern_ok := types.node(store, pattern)
if pattern_ok && pattern_item.qualifier == 0 && pattern_item.name != 0 {
name := symbol.Id(pattern_item.name)
if binding_index, is_binding := comptime_binding_index(function, prefix, name); is_binding {
param, param_ok := comptime_param_for_name(function, name)
if !param_ok || !is_comptime_type_param(checker, param) {
return false
}
if bound[binding_index] && can_implicitly_convert_type(checker, actual_type, values[binding_index].type) {
return true
}
return bind_inferred_comptime(
checker, function, prefix, values, bound, name,
Comptime_Value{type=actual_type, kind=.Type}, span, diagnose,
)
}
}
if !pattern_ok {
return types.equal(pattern, actual_type)
}
if pattern_item.kind == .Type_Call {
expr_id := ast.Expr_Id(pattern_item.count_expr)
if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) {
return false
}
expr := checker.ast_module.exprs[expr_id]
target_pkg, available := expr_package(checker, expr, function.pkg, function.file, true)
if !available {
return false
}
template := find_template(checker, expr.name, target_pkg, expr_lookup_file(expr, function.file))
origin: Type_Factory_Origin
found := false
ambiguous := false
for candidate in checker.type_factory_origins {
if candidate.template != template || !types.equal(candidate.result, actual_type) ||
len(expr.args) != len(candidate.values) {
continue
}
compatible := true
for arg_id, index in expr.args {
if arg_id == ast.INVALID_EXPR || int(arg_id) >= len(checker.ast_module.exprs) {
compatible = false
break
}
arg := checker.ast_module.exprs[arg_id]
if arg.kind == .Name && !symbol.is_valid(arg.qualifier) {
if binding_index, is_binding := comptime_binding_index(function, prefix, arg.name);
is_binding && bound[binding_index] &&
!inferred_comptime_value_equal(checker, values[binding_index], candidate.values[index]) {
compatible = false
break
}
}
}
if !compatible {
continue
}
if !found {
origin = candidate
found = true
continue
}
for arg_id, index in expr.args {
arg := checker.ast_module.exprs[arg_id]
if arg.kind == .Name && !symbol.is_valid(arg.qualifier) {
if _, is_binding := comptime_binding_index(function, prefix, arg.name);
is_binding && !inferred_comptime_value_equal(checker, origin.values[index], candidate.values[index]) {
ambiguous = true
break
}
}
}
}
if !found || ambiguous {
return false
}
matched := true
for arg_id, index in expr.args {
arg := checker.ast_module.exprs[arg_id]
if arg.kind == .Name && !symbol.is_valid(arg.qualifier) {
if _, is_binding := comptime_binding_index(function, prefix, arg.name); is_binding {
matched = bind_inferred_comptime(
checker, function, prefix, values, bound, arg.name,
origin.values[index], span, diagnose,
) && matched
}
}
}
return matched
}
actual_item, actual_ok := types.node(store, actual_type)
if pattern_item.kind == .Slice {
actual_pointer, actual_array, array_pointer_ok := types.array_pointer(actual_type, store)
if array_pointer_ok {
mutable := actual_pointer.mutable && actual_array.mutable
if pattern_item.mutable && !mutable ||
pattern_item.has_sentinel && (!actual_array.has_sentinel || pattern_item.sentinel != actual_array.sentinel) {
return false
}
return match_inferred_type_pattern(
checker, function, prefix, pattern_item.child, actual_array.child,
values, bound, span, diagnose, depth+1,
)
}
}
if !actual_ok || pattern_item.kind != actual_item.kind {
resolved := type_from_syntax(checker, pattern, function.pkg, function.file)
return types.is_valid(resolved) && types.equal(types.resolve_alias(resolved, store), actual_type)
}
if pattern_item.many != actual_item.many || pattern_item.has_sentinel != actual_item.has_sentinel ||
pattern_item.has_sentinel && pattern_item.sentinel != actual_item.sentinel {
return false
}
matched := true
if pattern_item.kind == .Array {
if pattern_item.unresolved_count {
count_expr := ast.Expr_Id(pattern_item.count_expr)
if count_expr != ast.INVALID_EXPR && int(count_expr) < len(checker.ast_module.exprs) {
expr := checker.ast_module.exprs[count_expr]
if expr.kind == .Name && !symbol.is_valid(expr.qualifier) {
if binding_index, is_binding := comptime_binding_index(function, prefix, expr.name); is_binding {
param, param_ok := comptime_param_for_name(function, expr.name)
if param_ok && !is_comptime_type_param(checker, param) {
matched = bind_inferred_comptime(
checker, function, prefix, values, bound, expr.name,
Comptime_Value{type=values[binding_index].type, value=i128(actual_item.count), kind=.Integer},
span, diagnose,
) && matched
}
}
}
}
} else if pattern_item.count != actual_item.count {
matched = false
}
}
if types.is_valid(pattern_item.child) {
matched = match_inferred_type_pattern(
checker, function, prefix, pattern_item.child, actual_item.child,
values, bound, span, diagnose, depth+1,
) && matched
}
if types.is_valid(pattern_item.extra) {
matched = match_inferred_type_pattern(
checker, function, prefix, pattern_item.extra, actual_item.extra,
values, bound, span, diagnose, depth+1,
) && matched
}
if pattern_item.kind == .Function {
pattern_params := types.params_for(store, pattern)
actual_params := types.params_for(store, actual_type)
if len(pattern_params) != len(actual_params) || pattern_item.c_abi != actual_item.c_abi || pattern_item.variadic != actual_item.variadic {
return false
}
for field, index in pattern_params {
matched = match_inferred_type_pattern(
checker, function, prefix, field.type, actual_params[index].type,
values, bound, span, diagnose, depth+1,
) && matched
}
}
return matched
}
infer_call_comptime_values :: proc(
checker: ^Checker,
function: ast.Function,
prefix: int,
mapping: []int,
args: []ast.Expr_Id,
actual_args: []types.Type,
expected: types.Type,
pkg: ast.Package_Id,
file: ast.File_Id,
diagnose := false,
failure: ^string = nil,
) -> ([]Comptime_Value, bool) {
values := make([]Comptime_Value, prefix, checker.allocator)
bound := make([]bool, prefix, checker.allocator)
defer delete(bound, checker.allocator)
ordinal := 0
for param in function.params {
if !param.comptime_value {
continue
}
values[ordinal].name = param.name
dependent := type_pattern_mentions_comptime(checker, function, ordinal, param.type)
if dependent {
values[ordinal].kind = .Static
} else if is_comptime_type_param(checker, param) {
values[ordinal].kind = .Type
} else if is_comptime_string_param(checker, param, function) {
values[ordinal].kind = .String
values[ordinal].type = type_from_syntax(checker, param.type, function.pkg, function.file)
} else if types.is_concrete_integer(type_from_syntax(checker, param.type, function.pkg, function.file)) {
values[ordinal].kind = .Integer
values[ordinal].type = type_from_syntax(checker, param.type, function.pkg, function.file)
} else {
values[ordinal].kind = .Static
values[ordinal].type = type_from_syntax(checker, param.type, function.pkg, function.file)
}
ordinal += 1
}
matched := true
all_bound := true
for value_bound in bound {
all_bound = all_bound && value_bound
}
if !all_bound && is_runtime_type(checker, expected) {
if types.is_valid(function.error) && types.kind(expected, &checker.module.types) == .Fallible {
matched = match_inferred_type_pattern(
checker, function, prefix, function.result,
types.fallible_success(expected, &checker.module.types), values, bound,
source.Span{}, diagnose,
) && matched
matched = match_inferred_type_pattern(
checker, function, prefix, function.error,
types.fallible_error(expected, &checker.module.types), values, bound,
source.Span{}, diagnose,
) && matched
} else if type_pattern_mentions_comptime(checker, function, prefix, function.result) {
matched = match_inferred_type_pattern(
checker, function, prefix, function.result, expected, values, bound,
source.Span{}, diagnose,
) && matched
}
}
// A comptime aggregate is evaluated only after its dependent type is known.
// Bind a direct `[N]T` parameter's N from an array literal's syntax first;
// this supplies the contextual element type for strings and enum literals.
for arg_id, source_index in args {
param_index := call_param_index(mapping, source_index)
if param_index < 0 || param_index >= len(function.params) ||
!function.params[param_index].comptime_value ||
arg_id == ast.INVALID_EXPR || int(arg_id) >= len(checker.ast_module.exprs) ||
checker.ast_module.exprs[arg_id].kind != .Array {
continue
}
pattern, pattern_ok := types.node(&checker.module.types, function.params[param_index].type)
if !pattern_ok || pattern.kind != .Array || !pattern.unresolved_count ||
pattern.count_expr == u32(ast.INVALID_EXPR) || int(pattern.count_expr) >= len(checker.ast_module.exprs) {
continue
}
count_expr := checker.ast_module.exprs[ast.Expr_Id(pattern.count_expr)]
if count_expr.kind != .Name || symbol.is_valid(count_expr.qualifier) {
continue
}
binding_index, is_binding := comptime_binding_index(function, prefix, count_expr.name)
if !is_binding || bound[binding_index] {
continue
}
param, param_ok := comptime_param_for_name(function, count_expr.name)
if !param_ok || is_comptime_type_param(checker, param) {
continue
}
values[binding_index] = Comptime_Value{
name=count_expr.name,
type=type_from_syntax(checker, param.type, function.pkg, function.file),
value=i128(len(checker.ast_module.exprs[arg_id].args)),
kind=.Integer,
}
bound[binding_index] = true
}
for arg_id, source_index in args {
param_index := call_param_index(mapping, source_index)
if param_index < 0 || param_index >= len(function.params) || !function.params[param_index].comptime_value ||
arg_id == ast.INVALID_EXPR || int(arg_id) >= len(checker.ast_module.exprs) {
continue
}
expr := checker.ast_module.exprs[arg_id]
if expr.kind == .Inference_Hole {
continue
}
param := function.params[param_index]
binding_index, binding_ok := comptime_binding_index(function, prefix, param.name)
if !binding_ok {
matched = false
continue
}
dependent := type_pattern_mentions_comptime(checker, function, prefix, param.type)
if is_comptime_type_param(checker, param) {
actual, ok := resolve_type_argument(checker, arg_id, pkg, file)
if !ok {
if failure != nil && len(failure^) == 0 {
failure^ = fmt.aprintf(
"argument %d for comptime type parameter '%s' is not a type",
source_index+1, symbol_text(checker, param.name), allocator=checker.allocator,
)
}
if diagnose {
source.addf(checker.diagnostics, expr.span,
"argument for comptime type parameter '%s' must be a type",
symbol_text(checker, param.name))
}
matched = false
continue
}
values[binding_index] = Comptime_Value{name=param.name, type=actual, kind=.Type}
bound[binding_index] = true
} else if !dependent && is_comptime_string_param(checker, param, function) {
text, text_ok := comptime_string_argument(checker, arg_id, pkg, file)
if !text_ok {
if failure != nil && len(failure^) == 0 {
failure^ = fmt.aprintf(
"argument %d for comptime string parameter '%s' does not evaluate to immutable bytes",
source_index+1, symbol_text(checker, param.name), allocator=checker.allocator,
)
}
if diagnose {
source.addf(checker.diagnostics, expr.span,
"argument for comptime string parameter '%s' must evaluate to immutable bytes",
symbol_text(checker, param.name))
}
matched = false
continue
}
values[binding_index] = Comptime_Value{
name=param.name,
type=type_from_syntax(checker, param.type, function.pkg, function.file),
text=text,
kind=.String,
}
bound[binding_index] = true
} else if !dependent && types.is_concrete_integer(type_from_syntax(checker, param.type, function.pkg, function.file)) {
declared := type_from_syntax(checker, param.type, function.pkg, function.file)
constant := eval_integer_constant_in_context(checker, arg_id, pkg, file)
if constant.kind != .Value {
if failure != nil && len(failure^) == 0 {
failure^ = fmt.aprintf(
"argument %d for comptime parameter '%s' is not a compile-time integer expression",
source_index+1, symbol_text(checker, param.name), allocator=checker.allocator,
)
}
if diagnose {
source.addf(checker.diagnostics, expr.span,
"argument for comptime parameter '%s' must be a compile-time integer expression",
symbol_text(checker, param.name))
}
matched = false
continue
}
if !fits_integer_type(constant.value, declared, checker.target) {
if failure != nil && len(failure^) == 0 {
failure^ = fmt.aprintf(
"argument %d integer constant %d does not fit in %s",
source_index+1, constant.value, types.name(declared), allocator=checker.allocator,
)
}
if diagnose {
source.addf(checker.diagnostics, expr.span,
"integer constant %d does not fit in %s", constant.value, types.name(declared))
}
matched = false
continue
}
values[binding_index] = Comptime_Value{name=param.name, type=declared, value=constant.value, kind=.Integer}
bound[binding_index] = true
} else {
available: [dynamic]Comptime_Value
available.allocator = checker.allocator
append(&available, ..checker.current_comptime_values)
for prior, prior_index in values[:binding_index] {
if bound[prior_index] {
append(&available, prior)
}
}
previous := checker.current_comptime_values
checker.current_comptime_values = available[:]
declared := type_from_syntax(checker, param.type, function.pkg, function.file)
checker.current_comptime_values = previous
value, value_ok := eval_static_comptime_value(
checker, param.name, arg_id, declared, pkg, file, available[:], diagnose,
)
delete(available)
if !value_ok {
if failure != nil && len(failure^) == 0 {
failure^ = fmt.aprintf(
"argument %d for comptime parameter '%s' has no stable comptime identity",
source_index+1, symbol_text(checker, param.name), allocator=checker.allocator,
)
}
matched = false
continue
}
values[binding_index] = value
bound[binding_index] = true
}
}
all_bound = true
for value_bound in bound {
all_bound = all_bound && value_bound
}
// Factory provenance is authoritative when unique, so try it before direct
// arguments. Strong and then weak direct evidence can disambiguate a factory
// result; retry factory patterns once after both evidence passes.
Evidence_Pass :: struct {
type_call: bool,
weak: bool,
final: bool,
}
evidence_passes := [?]Evidence_Pass{
{type_call=true},
{final=true},
{weak=true, final=true},
{type_call=true, final=true},
}
for pass in evidence_passes {
for arg_id, source_index in args {
param_index := call_param_index(mapping, source_index)
if param_index >= len(function.params) || param_index >= len(actual_args) {
continue
}
if param_index < 0 || function.params[param_index].comptime_value {
continue
}
arg_expr := checker.ast_module.exprs[arg_id]
is_null := arg_expr.kind == .Null
is_weak := is_numeric_constant_expr(checker, arg_id) ||
arg_expr.kind == .String || is_null
contains_type_call := type_pattern_contains_type_call(
checker, function.params[param_index].type,
)
if contains_type_call != pass.type_call ||
!pass.type_call && is_weak != pass.weak {
continue
}
if !type_pattern_mentions_comptime(checker, function, prefix, function.params[param_index].type) {
continue
}
// Once the result type has fixed every comptime parameter, an anonymous
// keyed record must be checked against the specialized parameter type.
// Its provisional structural type intentionally contains only the supplied
// fields, so comparing that type here would reject omitted defaulted fields.
if all_bound && arg_expr.kind == .Struct_Literal && !arg_expr.tuple && !symbol.is_valid(arg_expr.name) {
continue
}
if pass.weak {
if item, ok := types.node(&checker.module.types, function.params[param_index].type); ok &&
item.qualifier == 0 && item.name != 0 {
if binding_index, is_binding := comptime_binding_index(function, prefix, symbol.Id(item.name));
is_binding && bound[binding_index] {
continue
}
}
}
actual := actual_args[param_index]
if is_null {
previous := checker.current_comptime_values
checker.current_comptime_values = values
contextual := type_from_syntax(
checker, function.params[param_index].type, function.pkg, function.file,
)
checker.current_comptime_values = previous
if types.is_optional(contextual, &checker.module.types) {
actual = contextual
actual_args[param_index] = contextual
}
}
pass_matched := match_inferred_type_pattern(
checker, function, prefix, function.params[param_index].type,
actual, values, bound,
checker.ast_module.exprs[arg_id].span, diagnose && pass.final,
)
if pass.final {
matched = pass_matched && matched
}
}
}
ordinal = 0
for param in function.params {
if !param.comptime_value {
continue
}
if bound[ordinal] {
ordinal += 1
continue
}
matched = false
if failure != nil && len(failure^) == 0 {
failure^ = fmt.aprintf(
"cannot infer comptime parameter '%s'",
symbol_text(checker, param.name), allocator=checker.allocator,
)
}
if diagnose {
source.addf(
checker.diagnostics, param.span,
"cannot infer comptime parameter '%s'; pass it explicitly",
symbol_text(checker, param.name),
)
}
ordinal += 1
}
if !matched {
if failure != nil && len(failure^) == 0 {
failure^ = fmt.aprintf("comptime inference produced conflicting bindings", allocator=checker.allocator)
}
delete(values, checker.allocator)
return nil, false
}
return values, true
}
call_arg_expected :: proc(checker: ^Checker, function: ast.Function, index: int) -> types.Type {
if index < 0 || index >= len(function.params) {
return types.INVALID
}
if is_comptime_type_param(checker, function.params[index]) {
return types.INVALID
}
for param in function.params {
if !param.comptime_value {
continue
}
if _, ok := current_comptime_value(checker, param.name); !ok {
return types.INVALID
}
}
declared := type_from_syntax(checker, function.params[index].type, function.pkg, function.file)
// A `float` param defaults to f64 so an integer-literal argument builds as a
// float constant (e.g. `f(3)` -> 3.0), mirroring `pi float = 3` for locals.
// Integer/range constraints keep building naturally from their literals.
if declared == types.FLOAT {
return types.F64
}
return declared
}
resolve_type_argument :: proc(
checker: ^Checker,
expr_id: ast.Expr_Id,
pkg: ast.Package_Id,
file: ast.File_Id,
) -> (types.Type, bool) {
if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) {
return types.INVALID, false
}
expr := checker.ast_module.exprs[expr_id]
#partial switch expr.kind {
case .Type:
resolved := type_from_syntax(checker, expr.type, pkg, file)
return resolved, types.is_valid(resolved)
case .Name:
if symbol.is_valid(expr.qualifier) && symbol_text(checker, expr.name) == "type" {
if value, ok := static_field_value(checker, expr.qualifier, expr.name); ok && value.kind == .Type {
return types.Type(value.index), true
}
}
if !symbol.is_valid(expr.qualifier) {
if actual, ok := current_comptime_type(checker, expr.name); ok {
return actual, true
}
}
target_pkg, available := expr_package(checker, expr, pkg, file)
if !available {
return types.INVALID, false
}
value := types.find_named(&checker.module.types, u32(target_pkg), u32(expr.name), file=u32(expr_lookup_file(expr, file)))
value = types.resolve_alias(value, &checker.module.types)
return value, types.is_valid(value)
case .Call:
value := resolve_type_factory_call(checker, expr_id, pkg, file)
return value, types.is_valid(value)
}
return types.INVALID, false
}
clone_comptime_values :: proc(values: []Comptime_Value, allocator: mem.Allocator) -> []Comptime_Value {
result := make([]Comptime_Value, len(values), allocator)
copy(result, values)
return result
}
expand_expansions_equal :: proc(left, right: []Expand_Expansion) -> bool {
if len(left) != len(right) {
return false
}
for value, index in left {
if value != right[index] {
return false
}
}
return true
}
find_call_resolution :: proc(
checker: ^Checker,
expr: ast.Expr_Id,
) -> (int, bool) {
for index := len(checker.call_resolutions)-1; index >= 0; index -= 1 {
entry := checker.call_resolutions[index]
if entry.expr == expr &&
comptime_values_equal(entry.ctx, checker.current_comptime_values) &&
expand_expansions_equal(entry.expand_ctx, checker.expand_context[:]) {
return index, true
}
}
return -1, false
}
call_resolution_matches :: proc(
entry: Call_Resolution,
mapping: []int,
comptime_values: []Comptime_Value,
runtime_types: []types.Type,
) -> bool {
if len(entry.mapping) != len(mapping) || len(entry.runtime_types) != len(runtime_types) ||
!comptime_values_equal(entry.comptime_values, comptime_values) {
return false
}
for value, index in mapping {
if entry.mapping[index] != value {
return false
}
}
for value, index in runtime_types {
if !types.equal(entry.runtime_types[index], value) {
return false
}
}
return true
}
store_call_resolution :: proc(
checker: ^Checker,
expr: ast.Expr_Id,
mapping: []int,
comptime_values: []Comptime_Value,
runtime_types: []types.Type,
) -> int {
if index, ok := find_call_resolution(checker, expr); ok && call_resolution_matches(
checker.call_resolutions[index], mapping, comptime_values, runtime_types,
) {
return index
}
entry := Call_Resolution{
expr=expr,
ctx=clone_comptime_values(checker.current_comptime_values, checker.allocator),
expand_ctx=slice.clone(checker.expand_context[:], checker.allocator),
mapping=slice.clone(mapping, checker.allocator),
comptime_values=clone_comptime_values(comptime_values, checker.allocator),
runtime_types=slice.clone(runtime_types, checker.allocator),
folded_value=INVALID_CT_VALUE,
diagnostic=source.INVALID_DIAGNOSTIC,
}
if index, ok := find_call_resolution(checker, expr); ok {
previous := checker.call_resolutions[index]
delete(previous.ctx, checker.allocator)
delete(previous.expand_ctx, checker.allocator)
delete(previous.mapping, checker.allocator)
delete(previous.comptime_values, checker.allocator)
delete(previous.runtime_types, checker.allocator)
checker.call_resolutions[index] = entry
return index
}
append(&checker.call_resolutions, entry)
return len(checker.call_resolutions)-1
}
resolved_call_arg_expected :: proc(
checker: ^Checker,
function: ast.Function,
param_index: int,
resolution_index: int,
) -> types.Type {
if resolution_index < 0 || resolution_index >= len(checker.call_resolutions) {
return call_arg_expected(checker, function, param_index)
}
previous := checker.current_comptime_values
checker.current_comptime_values = checker.call_resolutions[resolution_index].comptime_values
result := call_arg_expected(checker, function, param_index)
checker.current_comptime_values = previous
return result
}
resolve_generated_struct_type :: proc(checker: ^Checker, expr_id: ast.Expr_Id, pkg: ast.Package_Id, file: ast.File_Id, active_state: ^Ct_State) -> types.Type {
for entry in checker.generated_types {
if entry.expr == expr_id && comptime_values_equal(entry.values, checker.current_comptime_values) {
return entry.result
}
}
if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) {
return types.INVALID
}
expr := checker.ast_module.exprs[expr_id]
field_start := int(u32(expr.integer>>32))
field_count := int(u32(expr.integer))
if field_start < 0 || field_count < 0 || field_start+field_count > len(checker.ast_module.type_fields) {
return types.INVALID
}
template_fields := checker.ast_module.type_fields[field_start:field_start+field_count]
fields := make([]types.Field, len(template_fields), checker.allocator)
defer delete(fields, checker.allocator)
for field, index in template_fields {
resolved := type_from_syntax(checker, field.type, pkg, file, active_state=active_state)
if (!is_runtime_type(checker, resolved) && !is_comptime_value_type(checker, resolved)) || types.is_void(resolved) {
if expr.tuple {
source.addf(checker.diagnostics, expr.span, "tuple element %d requires a concrete runtime type, got %s", index, type_label(checker, resolved))
} else {
source.addf(checker.diagnostics, expr.span, "anonymous struct field '%s' requires a concrete runtime type, got %s", symbol_text(checker, symbol.Id(field.name)), type_label(checker, resolved))
}
return types.INVALID
}
fields[index] = types.Field{name=field.name, type=resolved}
}
result := types.struct_generated(&checker.module.types, fields, expr.tuple)
append(&checker.generated_types, Generated_Type_Entry{
expr=expr_id,
values=clone_comptime_values(checker.current_comptime_values, checker.allocator),
result=result,
pkg=pkg,
file=file,
})
return result
}
resolve_type_factory_call :: proc(checker: ^Checker, expr_id: ast.Expr_Id, pkg: ast.Package_Id, file: ast.File_Id) -> types.Type {
if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) {
return types.INVALID
}
expr := checker.ast_module.exprs[expr_id]
if expr.kind != .Call || expr.left != ast.INVALID_EXPR {
source.add(checker.diagnostics, expr.span, "type position requires a direct type-factory call")
return types.INVALID
}
if expr.intrinsic {
if !is_intrinsic_call(checker, expr, "struct_type") {
return types.INVALID
}
state := ct_state_make(checker, pkg, file, values=checker.current_comptime_values)
value, flow, ok := ct_eval_call_expr(&state, expr, types.INVALID, 0, expr_id)
result := types.INVALID
if ok && flow.kind == .Normal && value != INVALID_CT_VALUE && int(value) < len(state.values) && state.values[value].kind == .Type {
result = types.Type(state.values[value].index)
}
ct_state_destroy(&state)
return result
}
target_pkg, available := expr_package(checker, expr, pkg, file, true)
if !available {
_ = add_package_resolution_diagnostic(checker, expr, file)
return types.INVALID
}
template := find_template(checker, expr.name, target_pkg, expr_lookup_file(expr, file))
if template == ast.INVALID_FUNCTION || int(template) >= len(checker.ast_module.functions) {
source.addf(checker.diagnostics, expr.span, "unknown type factory '%s'", symbol_text(checker, expr.name))
return types.INVALID
}
function := checker.ast_module.functions[template]
if !is_type_metatype_syntax(checker, function.result) || types.is_valid(function.error) {
source.addf(checker.diagnostics, expr.span, "function '%s' does not return a type", symbol_text(checker, expr.name))
return types.INVALID
}
for param in function.params {
if !param.comptime_value {
source.addf(checker.diagnostics, param.span, "type-factory parameter '%s' must be comptime", symbol_text(checker, param.name))
return types.INVALID
}
}
if !valid_call_arity(function, len(expr.args)) {
source.addf(checker.diagnostics, expr.span, "type factory '%s' expects %d arguments, got %d", symbol_text(checker, expr.name), len(function.params), len(expr.args))
return types.INVALID
}
values, ok := collect_comptime_values(checker, function, expr.args, pkg, file, true, checker.current_comptime_values)
defer delete(values, checker.allocator)
if !ok {
return types.INVALID
}
// A generic function's declaration is validated before it has a specialization.
// Leave calls containing its unresolved type parameters pending until then.
for value in values {
if value.kind != .Type {
continue
}
if item, item_ok := types.node(&checker.module.types, value.type); item_ok && item.kind == .Named && !item.declared {
return types.INVALID
}
}
for &entry in checker.type_factories {
if entry.template != template || !comptime_values_equal(entry.values, values) {
continue
}
if entry.resolving {
source.addf(checker.diagnostics, expr.span, "recursive type-factory specialization of '%s'", symbol_text(checker, expr.name))
return types.INVALID
}
return entry.result
}
entry_index := len(checker.type_factories)
append(&checker.type_factories, Type_Factory_Entry{
template=template,
values=clone_comptime_values(values, checker.allocator),
result=types.INVALID,
resolving=true,
})
state := ct_state_make(checker, pkg, file)
value, flow, eval_ok := ct_eval_call_expr(&state, expr, function.result, 0, expr_id)
result := types.INVALID
if eval_ok && flow.kind == .Normal && value != INVALID_CT_VALUE && int(value) < len(state.values) && state.values[value].kind == .Type {
result = types.Type(state.values[value].index)
} else if state.diagnostic == source.INVALID_DIAGNOSTIC {
source.addf(checker.diagnostics, expr.span, "type factory '%s' did not return a type", symbol_text(checker, expr.name))
}
ct_state_destroy(&state)
checker.type_factories[entry_index].result = result
checker.type_factories[entry_index].resolving = false
record_type_factory_origin(checker, result, template, values)
return result
}
record_type_factory_origin :: proc(
checker: ^Checker,
result: types.Type,
template: ast.Function_Id,
values: []Comptime_Value,
) {
if !types.is_valid(result) {
return
}
generated := false
for entry in checker.generated_types {
if types.equal(entry.result, result) {
generated = true
break
}
}
if !generated {
return
}
for origin in checker.type_factory_origins {
if origin.template == template && types.equal(origin.result, result) &&
comptime_values_equal(origin.values, values) {
return
}
}
append(&checker.type_factory_origins, Type_Factory_Origin{
result=result,
template=template,
values=clone_comptime_values(values, checker.allocator),
})
}
collect_comptime_values :: proc(
checker: ^Checker,
function: ast.Function,
args: []ast.Expr_Id,
pkg: ast.Package_Id,
file: ast.File_Id,
diagnose := false,
extra_values: []Comptime_Value = nil,
) -> ([]Comptime_Value, bool) {
if !function_has_comptime_params(function) {
return nil, true
}
values: [dynamic]Comptime_Value
values.allocator = checker.allocator
ok := true
for param, index in function.params {
if !param.comptime_value {
continue
}
span := param.span
if index < len(args) && args[index] != ast.INVALID_EXPR && int(args[index]) < len(checker.ast_module.exprs) {
span = checker.ast_module.exprs[args[index]].span
}
if is_comptime_type_param(checker, param) {
actual, actual_ok := types.INVALID, false
if index < len(args) {
actual, actual_ok = resolve_type_argument(checker, args[index], pkg, file)
}
if !actual_ok {
if diagnose {
source.addf(
checker.diagnostics,
span,
"argument for comptime type parameter '%s' must be a type",
symbol_text(checker, param.name),
)
}
ok = false
continue
}
append(&values, Comptime_Value{name=param.name, type=actual, kind=.Type})
continue
}
if is_comptime_string_param(checker, param, function) {
text, text_ok := "", false
if index < len(args) {
text, text_ok = comptime_string_argument(checker, args[index], pkg, file)
}
if !text_ok {
if diagnose {
source.addf(
checker.diagnostics, span,
"argument for comptime string parameter '%s' must evaluate to immutable bytes",
symbol_text(checker, param.name),
)
}
ok = false
continue
}
append(&values, Comptime_Value{
name=param.name,
type=type_from_syntax(checker, param.type, function.pkg, function.file),
text=text,
kind=.String,
})
continue
}
resolution_values: [dynamic]Comptime_Value
resolution_values.allocator = checker.allocator
append(&resolution_values, ..extra_values)
append(&resolution_values, ..values[:])
previous := checker.current_comptime_values
checker.current_comptime_values = resolution_values[:]
declared := type_from_syntax(checker, param.type, function.pkg, function.file)
checker.current_comptime_values = previous
delete(resolution_values)
if types.is_concrete_integer(declared) {
constant := Constant{kind = .Not_Constant}
if index < len(args) {
constant = eval_integer_constant_in_context(checker, args[index], pkg, file, values=extra_values)
}
if constant.kind != .Value {
if diagnose {
source.addf(
checker.diagnostics,
span,
"argument for comptime parameter '%s' must be a compile-time integer expression",
symbol_text(checker, param.name),
)
}
ok = false
continue
}
if !fits_integer_type(constant.value, declared, checker.target) {
if diagnose {
source.addf(
checker.diagnostics,
span,
"integer constant %d does not fit in %s",
constant.value,
types.name(declared),
)
}
ok = false
continue
}
append(&values, Comptime_Value{name=param.name, type=declared, value=constant.value})
continue
}
if index >= len(args) || args[index] == ast.INVALID_EXPR {
ok = false
continue
}
available: [dynamic]Comptime_Value
available.allocator = checker.allocator
append(&available, ..extra_values)
append(&available, ..values[:])
value, value_ok := eval_static_comptime_value(
checker, param.name, args[index], declared, pkg, file, available[:], diagnose,
)
delete(available)
if !value_ok {
ok = false
continue
}
append(&values, value)
}
if !ok {
delete(values)
return nil, false
}
return values[:], true
}
callable_arg_expected :: proc(function_type: types.Type, function_item: types.Node, store: ^types.Store, index: int) -> types.Type {
if index < 0 || index >= int(function_item.field_count) {
return types.INVALID
}
params := types.params_for(store, function_type)
if index >= len(params) {
return types.INVALID
}
return params[index].type
}
valid_callable_arity :: proc(function_item: types.Node, count: int) -> bool {
return count >= int(function_item.field_count) if function_item.variadic else count == int(function_item.field_count)
}
function_value_signature :: proc(
checker: ^Checker,
template: ast.Function_Id,
) -> (params: []types.Type, result: types.Type, ok: bool) {
if template == ast.INVALID_FUNCTION || int(template) >= len(checker.ast_module.functions) {
return nil, types.INVALID, false
}
function := checker.ast_module.functions[template]
if function_has_comptime_params(function) {
return nil, types.INVALID, false
}
if function.c_abi && types.is_valid(function.error) {
return nil, types.INVALID, false
}
if !function.c_abi && (!function.has_body || function.variadic) {
return nil, types.INVALID, false
}
result = function_channel_type(checker, function)
if !types.is_void(result) && !types.is_noreturn(result) && !is_runtime_type(checker, result) {
return nil, types.INVALID, false
}
params = make([]types.Type, len(function.params), checker.allocator)
for param, index in function.params {
param_type := type_from_syntax(checker, param.type, function.pkg, function.file)
if !is_runtime_type(checker, param_type) {
delete(params, checker.allocator)
return nil, types.INVALID, false
}
params[index] = param_type
}
return params, result, true
}
function_type_for_template :: proc(
checker: ^Checker,
template: ast.Function_Id,
demanded: ^[dynamic]Spec_Id = nil,
demand_spec := true,
) -> (types.Type, Spec_Id, bool) {
params, result, ok := function_value_signature(checker, template)
if !ok {
return types.INVALID, INVALID_SPEC, false
}
defer delete(params, checker.allocator)
function := checker.ast_module.functions[template]
function_type := types.function(&checker.module.types, params, result, function.c_abi, function.variadic)
spec := INVALID_SPEC
if demanded == nil {
if demand_spec {
spec = ensure_spec(checker, template, params)
} else {
spec = find_spec(checker, template, params)
}
} else {
spec = find_spec(checker, template, params)
if spec == INVALID_SPEC {
spec = ensure_spec(checker, template, params)
}
mark_spec_demanded(checker, spec, demanded)
}
return function_type, spec, spec != INVALID_SPEC || !demand_spec
}
function_pointer_type_for_template :: proc(
checker: ^Checker,
template: ast.Function_Id,
demanded: ^[dynamic]Spec_Id = nil,
demand_spec := true,
) -> (types.Type, Spec_Id, bool) {
function_type, spec, ok := function_type_for_template(checker, template, demanded, demand_spec)
if !ok {
return types.INVALID, spec, false
}
return types.pointer(&checker.module.types, function_type, false, false), spec, true
}
function_expr_type_for_template :: proc(
checker: ^Checker,
template: ast.Function_Id,
expected: types.Type,
demanded: ^[dynamic]Spec_Id = nil,
) -> (types.Type, bool) {
function_type, _, ok := function_type_for_template(checker, template, demanded)
if !ok {
return types.INVALID, false
}
pointer_expected := expected
if types.is_optional(pointer_expected, &checker.module.types) {
pointer_expected = types.child_type(pointer_expected, &checker.module.types)
}
if types.can_coerce_function_pointer(function_type, pointer_expected, &checker.module.types) {
return pointer_expected, true
}
return function_type, true
}
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: [dynamic]ast.Expr_Id
stack.allocator = checker.allocator
defer delete(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 is_intrinsic_call(checker, expr, "typeinfo") {
for &import_item in checker.ast_module.imports {
if import_item.valid && import_item.path == "@std/meta" {
import_item.used = true
}
}
}
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, .Bit_Not, .Address, .Deref, .Field, .Unwrap, .Try, .Keyed, .Enum_Literal, .Cast:
append(&stack, expr.left)
case .Comptime:
if expr.left != ast.INVALID_EXPR {
append(&stack, expr.left)
}
mark_block_imports_used(checker, expr.body, file)
case .Catch:
append(&stack, expr.left)
if expr.right != ast.INVALID_EXPR {
append(&stack, expr.right)
}
mark_block_imports_used(checker, expr.body, file)
case .Function_Literal:
function_id := ast.Function_Id(u32(expr.integer))
if function_id != ast.INVALID_FUNCTION && int(function_id) < len(checker.ast_module.functions) {
function := checker.ast_module.functions[function_id]
mark_block_imports_used(checker, function.body, function.file)
}
case .Add, .Sub, .Mul, .Div, .Bit_And, .Bit_Or, .Bit_Xor, .Shift_Left, .Shift_Right,
.Shift_Left_Saturating, .Index, .Orelse, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range:
append(&stack, expr.left, expr.right)
case .Invalid, .Integer, .Float, .String, .Bool, .Null, .Unreachable, .Undefined, .Inference_Hole, .Type, .Name, .Anonymous_Struct_Type:
}
}
}
mark_block_imports_used :: proc(checker: ^Checker, statements: []ast.Stmt_Id, file: ast.File_Id) {
for statement_id in statements {
statement := checker.ast_module.statements[statement_id]
#partial switch statement.kind {
case .Declaration, .Assignment, .Return, .Expression, .Yield:
mark_expr_imports_used(checker, statement.expr, file)
if statement.target != ast.INVALID_EXPR {
mark_expr_imports_used(checker, statement.target, file)
}
// A value-block declaration/assignment carries its block in `body`.
mark_block_imports_used(checker, statement.body, file)
case .If:
mark_expr_imports_used(checker, statement.expr, file)
if statement.guard != ast.INVALID_EXPR {
mark_expr_imports_used(checker, statement.guard, file)
}
mark_block_imports_used(checker, statement.body, file)
mark_block_imports_used(checker, statement.else_body, file)
case .While:
mark_expr_imports_used(checker, statement.expr, file)
mark_block_imports_used(checker, statement.body, file)
if statement.update != ast.INVALID_STMT {
update := [1]ast.Stmt_Id{statement.update}
mark_block_imports_used(checker, update[:], file)
}
case .For:
mark_expr_imports_used(checker, statement.expr, file)
mark_block_imports_used(checker, statement.body, file)
case .Block:
mark_block_imports_used(checker, statement.body, file)
case .Defer:
deferred := [1]ast.Stmt_Id{statement.update}
mark_block_imports_used(checker, deferred[:], file)
case .Match, .Match_Arm:
// `Match` carries the subject in `expr` and arms in `body`; each `Match_Arm`
// carries its patterns in `patterns` and the arm body in `body`.
mark_expr_imports_used(checker, statement.expr, file)
for pattern in statement.patterns {
mark_expr_imports_used(checker, pattern, file)
}
mark_block_imports_used(checker, statement.body, file)
case .Break, .Continue:
case .Invalid:
}
}
}
validate_external_globals :: proc(checker: ^Checker) {
for global, global_index in checker.ast_module.globals {
checker.external_global_canonical[global_index] = ast.global_id(global_index)
if !global.external {
continue
}
switch global.link_name {
case "main":
checker.external_global_diagnostics[global_index] = source.add(
checker.diagnostics,
global.span,
"external C variable 'main' conflicts with the program entry point",
)
case "write":
checker.external_global_diagnostics[global_index] = source.add(
checker.diagnostics,
global.span,
"external C variable 'write' conflicts with the compiler runtime",
)
}
for previous, previous_index in checker.ast_module.globals[:global_index] {
if !previous.external || previous.link_name != global.link_name {
continue
}
canonical := checker.external_global_canonical[previous_index]
if canonical == ast.INVALID_GLOBAL {
canonical = ast.global_id(previous_index)
}
canonical_index := int(canonical)
if canonical_index < 0 || canonical_index >= len(checker.ast_module.globals) {
canonical = ast.global_id(previous_index)
canonical_index = previous_index
}
checker.external_global_canonical[global_index] = canonical
canonical_global := checker.ast_module.globals[canonical_index]
canonical_type := checker.global_types[canonical_index]
if !types.equal(checker.global_types[global_index], canonical_type) ||
global.writable != canonical_global.writable {
checker.external_global_diagnostics[global_index] = source.addf(
checker.diagnostics,
global.span,
"conflicting external C variable declarations for '%s'",
global.link_name,
)
}
checker.global_types[global_index] = canonical_type
break
}
for function in checker.ast_module.functions {
if !function.c_abi || function.has_body || len(function.unsupported_reason) > 0 ||
symbol_text(checker, function.name) != global.link_name {
continue
}
if checker.external_global_diagnostics[global_index] == source.INVALID_DIAGNOSTIC {
checker.external_global_diagnostics[global_index] = source.addf(
checker.diagnostics,
global.span,
"external C variable '%s' conflicts with a C function declaration",
global.link_name,
)
}
break
}
}
}
runtime_write_declaration_matches :: proc(checker: ^Checker, function: ast.Function) -> bool {
if function.variadic || len(function.params) != 3 || types.is_valid(function.error) {
return false
}
store := &checker.module.types
buffer := types.optional(store, types.pointer(store, types.ANYOPAQUE, false, true))
return type_from_syntax(checker, function.params[0].type, function.pkg, function.file) == types.C_INT &&
type_from_syntax(checker, function.params[1].type, function.pkg, function.file) == buffer &&
type_from_syntax(checker, function.params[2].type, function.pkg, function.file) == types.C_ULONG &&
type_from_syntax(checker, function.result, function.pkg, function.file) == types.C_LONG
}
validate_declarations :: proc(checker: ^Checker) {
for function, function_id in checker.ast_module.functions {
if len(function.unsupported_reason) > 0 {
continue
}
has_comptime := function_has_comptime_params(function)
signature_poisoned := function.diagnostic != source.INVALID_DIAGNOSTIC
locals: [dynamic]symbol.Id
locals.allocator = checker.allocator
comptime_prefix := 0
for param in function.params {
dependent := param.comptime_value && type_pattern_mentions_comptime(
checker, function, comptime_prefix, param.type,
)
param_type := types.INVALID
if param.comptime_value && !dependent || !has_comptime {
param_type = type_from_syntax(checker, param.type, function.pkg, function.file)
}
if param.comptime_value && !signature_poisoned {
if function.c_abi {
checker.template_diagnostics[function_id] = source.add(
checker.diagnostics,
param.span,
"comptime parameters require 'func', not 'c_func'",
)
}
if !is_type_metatype_syntax(checker, param.type) &&
!dependent && !is_comptime_value_type(checker, param_type) && param_type != types.RANGE {
checker.template_diagnostics[function_id] = source.addf(
checker.diagnostics,
param.span,
"comptime parameter '%s' requires type or a concrete value type",
symbol_text(checker, param.name),
)
}
} else if !has_comptime && !signature_poisoned && types.is_comptime_only(param_type, &checker.module.types) {
checker.template_diagnostics[function_id] = source.addf(
checker.diagnostics,
param.span,
"parameter '%s' has a comptime-only type; prefix it with '$'",
symbol_text(checker, param.name),
)
} else if !has_comptime && !signature_poisoned {
if diagnostic := add_unsupported_type_diagnostic(checker, param.span, param_type);
diagnostic != source.INVALID_DIAGNOSTIC {
checker.template_diagnostics[function_id] = diagnostic
continue
}
}
if param.comptime_value {
comptime_prefix += 1
}
if param.type == types.VOID {
source.add(
checker.diagnostics,
param.span,
"void is only valid as a function result type",
)
}
if !param.comptime_value && types.is_noreturn(param_type) {
checker.template_diagnostics[function_id] = source.add(
checker.diagnostics,
param.span,
"noreturn is only valid as a native function result type",
)
}
if param.name != checker.sink_symbol && contains_name(locals[:], param.name) {
source.addf(
checker.diagnostics,
param.span,
"duplicate parameter '%s'",
symbol_text(checker, param.name),
)
} else {
_ = add_shadow_diagnostic(
checker, param.span, param.name, "parameter",
function.pkg, function.file,
)
}
append(&locals, param.name)
if !has_comptime && !signature_poisoned && types.contains_c_struct_by_value(param_type, &checker.module.types) {
checker.template_diagnostics[function_id] = source.addf(
checker.diagnostics,
param.span,
"C records cannot be passed by value to '%s'",
symbol_text(checker, function.name),
)
}
}
if !has_comptime && !signature_poisoned {
result_type := type_from_syntax(checker, function.result, function.pkg, function.file)
if function.c_abi && types.is_noreturn(result_type) {
checker.template_diagnostics[function_id] = source.add(
checker.diagnostics,
function.span,
"noreturn is not supported across the C ABI",
)
}
if diagnostic := add_unsupported_type_diagnostic(checker, function.span, result_type);
diagnostic != source.INVALID_DIAGNOSTIC {
checker.template_diagnostics[function_id] = diagnostic
}
if types.contains_c_struct_by_value(result_type, &checker.module.types) {
checker.template_diagnostics[function_id] = source.addf(
checker.diagnostics,
function.span,
"C records cannot be returned by value from '%s'",
symbol_text(checker, function.name),
)
}
}
if types.is_valid(function.error) && !signature_poisoned {
error_type := type_from_syntax(checker, function.error, function.pkg, function.file)
error_channel := types.is_enum(error_type, &checker.module.types) ||
types.is_struct(error_type, &checker.module.types) ||
types.is_tagged_union(error_type, &checker.module.types)
if function.c_abi {
checker.template_diagnostics[function_id] = source.add(
checker.diagnostics,
function.span,
"fallible functions must use 'func', not 'c_func'",
)
} else if !error_channel {
checker.template_diagnostics[function_id] = source.add(
checker.diagnostics,
function.span,
"fallible function error type must be a native enum, struct, or tagged union",
)
}
}
if !function.has_body && !function.c_abi && !signature_poisoned {
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) && !signature_poisoned {
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 && !signature_poisoned {
for param in function.params {
param_type := type_from_syntax(checker, param.type, function.pkg, function.file)
if add_unsupported_type_diagnostic(checker, param.span, param_type) !=
source.INVALID_DIAGNOSTIC {
continue
}
if types.contains_c_struct_by_value(param_type, &checker.module.types) {
continue
}
if !types.is_c_signature_type(param_type, &checker.module.types) {
checker.template_diagnostics[function_id] = source.addf(
checker.diagnostics,
param.span,
"foreign function '%s' requires concrete parameter types",
symbol_text(checker, function.name),
)
}
}
result := type_from_syntax(checker, function.result, function.pkg, function.file)
if add_unsupported_type_diagnostic(checker, function.span, result) == source.INVALID_DIAGNOSTIC &&
!types.is_noreturn(result) &&
!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",
)
}
external_name := function.link_name if len(function.link_name) > 0 else symbol_text(checker, function.name)
if external_name == "write" && !runtime_write_declaration_matches(checker, function) {
checker.template_diagnostics[function_id] = source.add(
checker.diagnostics,
function.span,
"external C function 'write' conflicts with the compiler runtime declaration",
)
}
}
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
}
}
}
init_record_field_inference :: proc(checker: ^Checker) {
store := &checker.module.types
for item in store.nodes {
if !(item.declared && (item.kind == .Struct || item.kind == .Union) &&
!item.c_layout && symbol.is_valid(symbol.Id(item.name))) {
continue
}
start := int(item.field_start)
end := start+int(item.field_count)
if start < 0 || end > len(store.fields) {
continue
}
for slot in start..<end {
if types.is_constraint(store.fields[slot].type) {
checker.record_field_constraints[slot] = store.fields[slot].type
}
}
}
}
is_inferred_record_field :: proc(checker: ^Checker, slot: int) -> bool {
return slot >= 0 && slot < len(checker.record_field_constraints) &&
types.is_constraint(checker.record_field_constraints[slot])
}
record_field_owner :: proc(checker: ^Checker, slot: int) -> (symbol.Id, symbol.Id, ast.Package_Id, bool) {
for item in checker.module.types.nodes {
if !(item.declared && (item.kind == .Struct || item.kind == .Union) &&
!item.c_layout && symbol.is_valid(symbol.Id(item.name))) {
continue
}
start := int(item.field_start)
if slot >= start && slot < start+int(item.field_count) &&
slot >= 0 && slot < len(checker.module.types.fields) {
return symbol.Id(item.name), symbol.Id(checker.module.types.fields[slot].name), ast.Package_Id(item.pkg), true
}
}
return symbol.INVALID, symbol.INVALID, ast.INVALID_PACKAGE, false
}
record_field_conflict :: proc(checker: ^Checker, slot: int, actual: types.Type, span: source.Span) {
if !is_inferred_record_field(checker, slot) ||
types.is_valid(checker.record_field_conflicts[slot]) {
return
}
checker.record_field_conflicts[slot] = actual
checker.record_field_conflict_spans[slot] = span
}
merge_record_field_default :: proc(checker: ^Checker, slot: int, candidate: types.Type, span: source.Span) -> bool {
if !is_inferred_record_field(checker, slot) || !is_runtime_type(checker, candidate) {
return false
}
constraint := checker.record_field_constraints[slot]
if !types.constraint_accepts(constraint, candidate, &checker.module.types) {
record_field_conflict(checker, slot, candidate, span)
return false
}
current := checker.record_field_defaults[slot]
if !is_runtime_type(checker, current) {
checker.record_field_defaults[slot] = candidate
checker.record_field_demands_dirty = true
return true
}
if types.equal(current, candidate) {
return false
}
merged := types.widest(current, candidate)
if types.is_concrete_scalar(merged) {
if !types.equal(current, merged) {
checker.record_field_defaults[slot] = merged
checker.record_field_demands_dirty = true
return true
}
return false
}
record_field_conflict(checker, slot, candidate, span)
return false
}
merge_record_field_demand :: proc(checker: ^Checker, slot: int, demand: types.Type, span: source.Span) -> bool {
if !is_inferred_record_field(checker, slot) || !is_runtime_type(checker, demand) {
return false
}
constraint := checker.record_field_constraints[slot]
if !types.constraint_accepts(constraint, demand, &checker.module.types) {
record_field_conflict(checker, slot, demand, span)
return false
}
current := checker.module.types.fields[slot].type
if types.is_constraint(current) {
checker.module.types.fields[slot].type = demand
checker.record_field_demands_dirty = true
return true
}
if types.equal(current, demand) {
return false
}
merged := types.widest(current, demand)
if types.is_concrete_scalar(merged) {
if !types.equal(current, merged) {
checker.module.types.fields[slot].type = merged
checker.record_field_demands_dirty = true
return true
}
return false
}
record_field_conflict(checker, slot, demand, span)
return false
}
record_field_expr_candidate :: proc(
checker: ^Checker,
slot: int,
expr_id: ast.Expr_Id,
inferred: types.Type,
locals: []Infer_Local,
pkg: ast.Package_Id,
file: ast.File_Id,
) -> bool {
if !is_inferred_record_field(checker, slot) || expr_id == ast.INVALID_EXPR {
return false
}
expr := checker.ast_module.exprs[expr_id]
constraint := checker.record_field_constraints[slot]
open := numeric_operand_is_open(checker, expr_id, locals, pkg, file)
if open && is_runtime_type(checker, checker.module.types.fields[slot].type) {
return false
}
if !open {
concrete := types.constraint_target(constraint, inferred, &checker.module.types)
if is_runtime_type(checker, concrete) {
return merge_record_field_demand(checker, slot, concrete, expr.span)
}
}
if constant := eval_integer_constant_in_context(checker, expr_id, pkg, file);
constant.kind == .Value {
candidate := constraint_integer_literal_type(constraint, constant.value)
if constraint == types.FLOAT {
candidate = types.F64 if fits_i64(constant.value) else types.INVALID
}
if types.is_valid(candidate) {
return merge_record_field_default(checker, slot, candidate, expr.span)
}
}
if is_float_constant_expr(checker, expr_id) {
return merge_record_field_default(checker, slot, types.F64, expr.span)
}
if open {
candidate := inferred
if constraint == types.FLOAT && types.is_concrete_integer(candidate) {
candidate = types.F64
}
return merge_record_field_default(checker, slot, candidate, expr.span)
}
concrete := types.constraint_target(constraint, inferred, &checker.module.types)
if !is_runtime_type(checker, concrete) {
if is_runtime_type(checker, inferred) {
record_field_conflict(checker, slot, inferred, expr.span)
}
return false
}
return merge_record_field_demand(checker, slot, concrete, expr.span)
}
finalize_record_field_inference :: proc(checker: ^Checker) {
for constraint, slot in checker.record_field_constraints {
if !types.is_constraint(constraint) {
continue
}
record_name, field_name, record_pkg, ok := record_field_owner(checker, slot)
if !ok {
continue
}
current := checker.module.types.fields[slot].type
conflict := checker.record_field_conflicts[slot]
if types.is_valid(conflict) {
if is_runtime_type(checker, current) {
source.addf(
checker.diagnostics,
checker.record_field_conflict_spans[slot],
"conflicting types %s and %s for field '%s.%s' declared as '%s'",
types.name(current), types.name(conflict),
symbol_text(checker, record_name), symbol_text(checker, field_name),
types.name(constraint),
)
} else {
source.addf(
checker.diagnostics,
checker.record_field_conflict_spans[slot],
"type %s does not satisfy the '%s' constraint for field '%s.%s'",
types.name(conflict), types.name(constraint),
symbol_text(checker, record_name), symbol_text(checker, field_name),
)
}
} else if types.is_constraint(current) &&
!(int(record_pkg) < len(checker.poisoned_packages) && checker.poisoned_packages[record_pkg]) {
source.addf(
checker.diagnostics,
source.Span{},
"could not resolve the '%s' constraint for field '%s.%s'",
types.name(constraint),
symbol_text(checker, record_name), symbol_text(checker, field_name),
)
}
if types.is_constraint(current) {
checker.module.types.fields[slot].type = constraint_recovery_type(checker, constraint)
}
}
}
is_immutable_u8_slice :: proc(checker: ^Checker, value: types.Type) -> bool {
item, ok := types.node(&checker.module.types, value)
return ok && item.kind == .Slice && !item.mutable && item.child == types.U8
}
validate_meta_schema :: proc(checker: ^Checker) {
meta_package := ast.INVALID_PACKAGE
for import_item in checker.ast_module.imports {
if import_item.valid && import_item.path == "@std/meta" {
meta_package = import_item.target
break
}
}
if meta_package == ast.INVALID_PACKAGE {
return
}
find := proc(checker: ^Checker, pkg: ast.Package_Id, name: string) -> types.Type {
return types.find_named(&checker.module.types, u32(pkg), u32(symbol.intern(checker.symbols, name)))
}
field_info := find(checker, meta_package, "FieldInfo")
array_info := find(checker, meta_package, "ArrayInfo")
record_info := find(checker, meta_package, "RecordInfo")
enum_info := find(checker, meta_package, "EnumInfo")
type_info := find(checker, meta_package, "TypeInfo")
layout := find(checker, meta_package, "Layout")
valid := types.is_valid(array_info) && types.is_valid(field_info) && types.is_valid(record_info) && types.is_valid(enum_info) &&
types.is_valid(type_info) && types.is_valid(layout)
layout_item, layout_ok := types.node(&checker.module.types, layout)
layout_members := types.enum_members_for(&checker.module.types, layout)
valid = valid && layout_ok && layout_item.kind == .Enum && len(layout_members) == 2
if valid {
valid = symbol_text(checker, symbol.Id(layout_members[0].name)) == "auto" &&
symbol_text(checker, symbol.Id(layout_members[1].name)) == "c"
}
array_item, array_ok := types.node(&checker.module.types, array_info)
field_item, field_ok := types.node(&checker.module.types, field_info)
record_item, record_ok := types.node(&checker.module.types, record_info)
enum_item, enum_ok := types.node(&checker.module.types, enum_info)
valid = valid && array_ok && array_item.kind == .Struct && !array_item.tuple && !array_item.c_layout &&
field_ok && field_item.kind == .Struct && !field_item.tuple && !field_item.c_layout &&
record_ok && record_item.kind == .Struct && !record_item.tuple && !record_item.c_layout &&
enum_ok && enum_item.kind == .Struct && !enum_item.tuple && !enum_item.c_layout
array_fields := types.fields_for(&checker.module.types, array_info)
valid = valid && len(array_fields) == 2
if valid {
valid = symbol_text(checker, symbol.Id(array_fields[0].name)) == "child" &&
is_type_metatype_syntax(checker, array_fields[0].type) &&
symbol_text(checker, symbol.Id(array_fields[1].name)) == "len" &&
types.equal(array_fields[1].type, types.USIZE)
}
field_fields := types.fields_for(&checker.module.types, field_info)
valid = valid && len(field_fields) == 3
if valid {
valid = symbol_text(checker, symbol.Id(field_fields[0].name)) == "name" &&
is_immutable_u8_slice(checker, field_fields[0].type) &&
symbol_text(checker, symbol.Id(field_fields[1].name)) == "type" &&
is_type_metatype_syntax(checker, field_fields[1].type) &&
symbol_text(checker, symbol.Id(field_fields[2].name)) == "index" &&
types.equal(field_fields[2].type, types.USIZE)
}
enum_fields := types.fields_for(&checker.module.types, enum_info)
valid = valid && len(enum_fields) == 1
if valid {
fields_item, fields_ok := types.node(&checker.module.types, enum_fields[0].type)
valid = symbol_text(checker, symbol.Id(enum_fields[0].name)) == "fields" && fields_ok &&
fields_item.kind == .Slice && !fields_item.mutable && types.equal(fields_item.child, field_info)
}
record_fields := types.fields_for(&checker.module.types, record_info)
valid = valid && len(record_fields) == 4
if valid {
name_type := types.child_type(record_fields[0].type, &checker.module.types)
fields_item, fields_ok := types.node(&checker.module.types, record_fields[1].type)
valid = symbol_text(checker, symbol.Id(record_fields[0].name)) == "name" &&
types.is_optional(record_fields[0].type, &checker.module.types) && is_immutable_u8_slice(checker, name_type) &&
symbol_text(checker, symbol.Id(record_fields[1].name)) == "fields" && fields_ok &&
fields_item.kind == .Slice && !fields_item.mutable && types.equal(fields_item.child, field_info) &&
symbol_text(checker, symbol.Id(record_fields[2].name)) == "is_tuple" && types.is_bool(record_fields[2].type) &&
symbol_text(checker, symbol.Id(record_fields[3].name)) == "layout" && types.equal(record_fields[3].type, layout)
}
type_item, type_ok := types.node(&checker.module.types, type_info)
type_fields := types.fields_for(&checker.module.types, type_info)
expected_tags := []string{
"invalid", "void", "noreturn", "anyopaque", "bool", "integer", "float", "array", "pointer", "slice",
"range", "optional", "function", "enum", "record", "union", "fallible", "distinct",
}
valid = valid && type_ok && type_item.kind == .Union &&
types.is_enum(type_item.child, &checker.module.types) && len(type_fields) == len(expected_tags)
if valid {
for tag, index in expected_tags {
field := type_fields[index]
if symbol_text(checker, symbol.Id(field.name)) != tag ||
(tag == "array" && !types.equal(field.type, array_info)) ||
(tag == "record" && !types.equal(field.type, record_info)) ||
(tag == "enum" && !types.equal(field.type, enum_info)) ||
(tag == "distinct" && !is_type_metatype_syntax(checker, field.type)) ||
(tag != "array" && tag != "record" && tag != "enum" && tag != "distinct" &&
!types.is_void(field.type)) {
valid = false
break
}
}
}
if !valid {
source.add(checker.diagnostics, source.Span{}, "@std/meta declarations do not match the compiler reflection ABI")
}
}
validate_type_nodes :: proc(checker: ^Checker) {
node_count := len(checker.module.types.nodes)
for index in 0..<node_count {
item := checker.module.types.nodes[index]
if item.kind != .Struct && item.kind != .Union {
continue
}
start := int(item.field_start)
end := start+int(item.field_count)
if start < 0 || end > len(checker.module.types.fields) {
continue
}
for slot in start..<end {
resolved := type_from_syntax(
checker, checker.module.types.fields[slot].type,
ast.Package_Id(item.pkg), ast.File_Id(item.file),
)
checker.module.types.fields[slot].type = resolved
}
}
validate_meta_schema(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 {
meta_package := false
for import_item in checker.ast_module.imports {
if import_item.valid && import_item.target == ast.Package_Id(item.pkg) && import_item.path == "@std/meta" {
meta_package = true
break
}
}
item_name := symbol_text(checker, symbol.Id(item.name))
comptime_meta := meta_package &&
(item_name == "ArrayInfo" || item_name == "FieldInfo" || item_name == "RecordInfo" || item_name == "EnumInfo" || item_name == "TypeInfo")
if item.c_layout && !item.opaque && item.field_count == 0 {
source.add(
checker.diagnostics,
source.Span{},
"c_struct definitions require at least one field",
)
}
// A tagged union may carry `void`-payload variants (`.quit void`): the
// variant has no runtime value, only a tag. Allowed only here, not for
// structs, untagged unions, or c_structs.
tagged_union := item.kind == .Union && types.is_enum(item.child, &checker.module.types)
for field, field_index in types.fields_for(&checker.module.types, id) {
field_slot := int(item.field_start)+field_index
if tagged_union && types.is_void(field.type) {
// void variant: no payload to validate.
} else if is_inferred_record_field(checker, field_slot) {
// Direct constraints in named native records are validated after the
// program-wide inference fixpoint. C, nested, and anonymous fields do
// not enter this state and retain the existing validation below.
} else if comptime_meta {
// Reflection metadata is compile-time-only and may contain `type`.
} else if item.c_layout && !types.is_runtime_value(field.type, &checker.module.types) {
source.add(
checker.diagnostics,
source.Span{},
"c_struct fields must have C-layout-compatible types",
)
} else if !types.is_runtime_value(field.type, &checker.module.types) &&
!is_comptime_value_type(checker, field.type) {
source.add(
checker.diagnostics,
source.Span{},
"record fields must have runtime value types",
)
} else if item.c_layout && !types.is_c_record_field_type(field.type, &checker.module.types) {
source.add(
checker.diagnostics,
source.Span{},
"c_struct fields must have C-layout-compatible types",
)
}
}
// A tagged union stores a hidden runtime tag enum keyed by global variant IDs.
// An explicit `union(Enum)` keeps that declared enum only for validation.
if item.kind == .Union && types.is_valid(item.child) {
declared_tag := types.union_declared_tag_enum(id, &checker.module.types)
if !types.is_enum(declared_tag, &checker.module.types) {
source.add(checker.diagnostics, source.Span{}, "a tagged union's tag must be an enum")
} else {
for field in types.fields_for(&checker.module.types, id) {
if _, ok := find_enum_member(checker, declared_tag, symbol.Id(field.name)); !ok {
source.addf(
checker.diagnostics,
source.Span{},
"union variant '%s' is not a member of the tag enum",
symbol_text(checker, symbol.Id(field.name)),
)
}
}
}
}
}
if item.kind == .Function {
unresolved := type_contains_unresolved_named(checker, item.child)
for param in types.params_for(&checker.module.types, id) {
unresolved = unresolved || type_contains_unresolved_named(checker, param.type)
}
if unresolved {
continue
}
if item.c_abi {
if types.kind(item.child, &checker.module.types) == .Fallible {
source.add(checker.diagnostics, source.Span{}, "c_func pointer results cannot be fallible")
}
for param in types.params_for(&checker.module.types, id) {
if types.is_void(param.type) || !types.is_c_signature_type(param.type, &checker.module.types) {
source.add(checker.diagnostics, source.Span{}, "c_func pointer parameters must be concrete C signature types")
}
}
if !types.is_c_signature_type(item.child, &checker.module.types, true) {
source.add(checker.diagnostics, source.Span{}, "c_func pointer results must be concrete C signature types or void")
}
} else {
if item.variadic {
source.add(checker.diagnostics, source.Span{}, "native function pointer types do not support variadic parameters")
}
for param in types.params_for(&checker.module.types, id) {
if types.is_void(param.type) || !is_runtime_type(checker, param.type) {
source.add(checker.diagnostics, source.Span{}, "native function pointer parameters must be concrete runtime types")
}
}
if !types.is_void(item.child) && !types.is_noreturn(item.child) && !is_runtime_type(checker, item.child) {
source.add(checker.diagnostics, source.Span{}, "native function pointer results must be concrete runtime types, void, or noreturn")
}
}
}
}
}
find_infer_local :: proc(locals: []Infer_Local, name: symbol.Id) -> types.Type {
if index, ok := find_infer_local_index(locals, name); ok {
return locals[index].type
}
return types.INVALID
}
find_infer_local_index :: proc(locals: []Infer_Local, name: symbol.Id) -> (int, bool) {
for index := len(locals) - 1; index >= 0; index -= 1 {
if locals[index].name == name {
return index, true
}
}
return -1, false
}
find_spec :: proc(
checker: ^Checker,
template: ast.Function_Id,
actual_args: []types.Type,
comptime_values: []Comptime_Value = nil,
) -> Spec_Id {
function := checker.ast_module.functions[template]
for spec, index in checker.specs {
if spec.template != template || len(spec.args) != runtime_param_count(function) ||
!comptime_values_equal(spec.comptime_values, comptime_values) {
continue
}
matches := true
runtime_index := 0
previous_comptime := checker.current_comptime_values
checker.current_comptime_values = comptime_values
for param, param_index in function.params {
if param.comptime_value {
continue
}
actual := types.INVALID
if param_index < len(actual_args) {
actual = actual_args[param_index]
}
if runtime_index >= len(spec.args) ||
!types.equal(spec.args[runtime_index], specialized_param_type(checker, param.type, actual, function.pkg, function.file)) {
matches = false
break
}
runtime_index += 1
}
checker.current_comptime_values = previous_comptime
if matches {
return spec_id(index)
}
}
return INVALID_SPEC
}
// specialized_param_type maps a parameter's declared type to its monomorphized
// type for a given actual argument. A constraint param (`int`/`uint`/`float`/`range`)
// resolves to the actual's family member (INVALID if out of family), so a call
// passing an out-of-family argument fails to specialize and is rejected.
specialized_param_type :: proc(
checker: ^Checker,
syntax: ast.Type_Syntax,
actual: types.Type,
pkg: ast.Package_Id,
file: ast.File_Id,
) -> types.Type {
declared := type_from_syntax(checker, syntax, pkg, file)
if types.is_constraint(declared) {
return types.constraint_target(declared, actual, &checker.module.types)
}
return declared
}
can_specialize :: proc(
checker: ^Checker,
function: ast.Function,
actual_args: []types.Type,
comptime_values: []Comptime_Value = nil,
) -> bool {
previous_comptime := checker.current_comptime_values
checker.current_comptime_values = comptime_values
defer checker.current_comptime_values = previous_comptime
for param, index in function.params {
if param.comptime_value {
continue
}
actual := types.INVALID
if index < len(actual_args) {
actual = actual_args[index]
}
if !is_runtime_type(checker, specialized_param_type(checker, param.type, actual, function.pkg, function.file)) {
return false
}
}
return true
}
ensure_spec :: proc(
checker: ^Checker,
template: ast.Function_Id,
actual_args: []types.Type,
comptime_values: []Comptime_Value = nil,
) -> Spec_Id {
if existing := find_spec(checker, template, actual_args, comptime_values); existing != INVALID_SPEC {
return existing
}
function := checker.ast_module.functions[template]
previous_comptime := checker.current_comptime_values
checker.current_comptime_values = comptime_values
defer checker.current_comptime_values = previous_comptime
signature: [dynamic]types.Type
signature.allocator = checker.allocator
for param, index in function.params {
if param.comptime_value {
continue
}
actual := types.INVALID
if index < len(actual_args) {
actual = actual_args[index]
}
append(&signature, specialized_param_type(checker, param.type, actual, function.pkg, function.file))
}
comptime_signature: [dynamic]Comptime_Value
comptime_signature.allocator = checker.allocator
append(&comptime_signature, ..comptime_values)
result := function_channel_type(checker, function)
index := spec_id(len(checker.specs))
append(
&checker.specs,
Spec{
template = template,
args = signature[:],
comptime_values = comptime_signature[:],
result = result,
hir_id = hir.INVALID_FUNCTION,
},
)
return index
}
mark_spec_demanded :: proc(checker: ^Checker, id: Spec_Id, stack: ^[dynamic]Spec_Id) {
if id == INVALID_SPEC || checker.specs[id].hir_id != hir.INVALID_FUNCTION {
return
}
checker.specs[id].hir_id = hir.Function_Id(0)
append(stack, id)
}
Numeric_Operation_Type :: struct {
result: types.Type,
representation: types.Type,
}
numeric_operation_type :: proc(checker: ^Checker, left, right: types.Type) -> (Numeric_Operation_Type, bool) {
if types.equal(left, right) {
if representation, ok := types.distinct_scalar_backing(left, &checker.module.types); ok {
return Numeric_Operation_Type{result=left, representation=representation}, true
}
}
result := types.widest(left, right)
if !types.is_concrete_scalar(result) {
return {}, false
}
return Numeric_Operation_Type{result=result, representation=result}, true
}
numeric_literal_accepts_type :: proc(
checker: ^Checker,
expr: ast.Expr_Id,
demand: types.Type,
pkg: ast.Package_Id,
file: ast.File_Id,
) -> bool {
representation := demand
if backing, ok := types.distinct_scalar_backing(demand, &checker.module.types); ok {
representation = backing
}
if constant := eval_integer_constant_in_context(checker, expr, pkg, file); constant.kind == .Value {
return types.is_float(representation, checker.target) ||
types.is_concrete_integer(representation) &&
fits_integer_type(constant.value, representation, checker.target)
}
return is_float_constant_expr(checker, expr) && types.is_float(representation, checker.target)
}
Infer_Frame :: struct {
expr: ast.Expr_Id,
expected: types.Type,
stage: u8,
left: types.Type,
arg_index: int,
arg_mode: Call_Argument_Mode,
prefix: int,
mapping: []int,
args: []types.Type,
template: ast.Function_Id,
numeric_operation: bool,
reverse_operands: bool,
}
merge_inferred_test_error :: proc(checker: ^Checker, incoming: types.Type) {
current := checker.inferred_test_error
if current == nil || !types.is_valid(incoming) ||
types.can_sum_widen(incoming, current^, &checker.module.types) {
return
}
if merged, err := types.compose_sum(&checker.module.types, current^, incoming); err == .None {
current^ = merged
}
}
infer_nested_expr :: proc(
checker: ^Checker,
expr_id: ast.Expr_Id,
locals: []Infer_Local,
pkg: ast.Package_Id,
file: ast.File_Id,
demanded: ^[dynamic]Spec_Id,
local_types: []types.Type = nil,
expected := types.INVALID,
) -> types.Type {
outer := checker.infer_stack
checker.infer_stack = nil
checker.infer_stack.allocator = checker.allocator
result := infer_expr(checker, expr_id, locals, pkg, file, demanded, local_types, expected)
delete(checker.infer_stack)
checker.infer_stack = outer
return result
}
infer_division_builtin :: 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,
expected: types.Type,
) -> types.Type {
if len(expr.args) != 2 {
return types.INVALID
}
hint := expected if types.is_concrete_scalar(expected) && !types.is_bool(expected) else types.INVALID
left_const := is_numeric_constant_expr(checker, expr.args[0])
right_const := is_numeric_constant_expr(checker, expr.args[1])
left, right := types.INVALID, types.INVALID
if left_const && !right_const && !types.is_valid(hint) {
right = infer_nested_expr(checker, expr.args[1], locals, pkg, file, demanded, local_types)
left = infer_nested_expr(checker, expr.args[0], locals, pkg, file, demanded, local_types, right)
} else {
left = infer_nested_expr(checker, expr.args[0], locals, pkg, file, demanded, local_types, hint)
right_hint := hint if types.is_valid(hint) else left
right = infer_nested_expr(checker, expr.args[1], locals, pkg, file, demanded, local_types, right_hint)
}
if left_const && types.is_distinct(right, &checker.module.types) &&
numeric_literal_accepts_type(checker, expr.args[0], right, pkg, file) {
left = right
}
if right_const && types.is_distinct(left, &checker.module.types) &&
numeric_literal_accepts_type(checker, expr.args[1], left, pkg, file) {
right = left
}
operation, ok := numeric_operation_type(checker, left, right)
return operation.result if ok && !types.is_bool(operation.representation) else types.INVALID
}
infer_compound_expr :: proc(
checker: ^Checker,
expr: ast.Expr,
locals: []Infer_Local,
pkg: ast.Package_Id,
file: ast.File_Id,
demanded: ^[dynamic]Spec_Id,
local_types: []types.Type = nil,
expected := types.INVALID,
) -> types.Type {
store := &checker.module.types
#partial switch expr.kind {
case .Comptime:
return infer_comptime_expr_type(checker, expr, pkg, file, demanded)
case .Bool:
return types.BOOL
case .Not:
_ = infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
return types.BOOL
case .Bit_Not:
operand := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
representation := types.runtime_representation(operand, store)
return operand if types.is_concrete_integer(representation) else types.INVALID
case .Bit_And, .Bit_Or, .Bit_Xor:
left_const := is_numeric_constant_expr(checker, expr.left)
right_const := is_numeric_constant_expr(checker, expr.right)
left, right := types.INVALID, types.INVALID
if types.is_concrete_integer(expected) {
left = infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types, expected)
right = infer_nested_expr(checker, expr.right, locals, pkg, file, demanded, local_types, expected)
} else if left_const && !right_const {
right = infer_nested_expr(checker, expr.right, locals, pkg, file, demanded, local_types)
left = infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
} else {
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)
}
if left_const && types.is_distinct(right, store) &&
numeric_literal_accepts_type(checker, expr.left, right, pkg, file) {
left = right
}
if right_const && types.is_distinct(left, store) &&
numeric_literal_accepts_type(checker, expr.right, left, pkg, file) {
right = left
}
operation, ok := numeric_operation_type(checker, left, right)
return operation.result if ok && types.is_concrete_integer(operation.representation) else types.INVALID
case .Shift_Left, .Shift_Right, .Shift_Left_Saturating:
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, types.U64)
representation := types.runtime_representation(left, store)
return left if types.is_concrete_integer(representation) && types.is_unsigned(right, checker.target) else types.INVALID
case .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or:
left_expr := checker.ast_module.exprs[expr.left]
right_expr := checker.ast_module.exprs[expr.right]
if left_expr.kind == .Null && right_expr.kind != .Null {
right := infer_nested_expr(checker, expr.right, locals, pkg, file, demanded, local_types)
_ = infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types, right)
} else if right_expr.kind == .Null && left_expr.kind != .Null {
left := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
_ = infer_nested_expr(checker, expr.right, locals, pkg, file, demanded, local_types, left)
} else {
_ = 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 .Null:
return expected if types.is_optional(expected, store) else types.INVALID
case .Unreachable:
return types.NORETURN
case .Undefined:
return types.INVALID
case .Enum_Literal:
if expr.left != ast.INVALID_EXPR {
_ = infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
}
return types.INVALID
case .Cast:
_ = infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
return type_from_syntax(checker, expr.type, pkg, file)
case .Address:
child := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
return types.pointer(store, child, false, false)
case .Deref:
value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
return types.child_type(value, store) if types.is_pointer(value, store) else types.INVALID
case .Index:
value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
_ = infer_nested_expr(checker, expr.right, locals, pkg, file, demanded, local_types)
item, ok := types.container(value, store)
return item.child if ok else types.INVALID
case .Slice:
text, comptime_ok := comptime_slice_bytes(checker, expr, pkg, file)
if comptime_ok {
string_id := intern_comptime_string(checker, text)
return string_literal_type(checker, string_id)
}
value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
item, ok := types.container(value, store)
if !ok || (item.kind == .Pointer && expr.args[1] == ast.INVALID_EXPR) {
return types.INVALID
}
for bound in expr.args {
if bound != ast.INVALID_EXPR {
_ = infer_nested_expr(checker, bound, locals, pkg, file, demanded, local_types)
}
}
preserve := item.has_sentinel && expr.args[1] == ast.INVALID_EXPR
return types.slice(store, item.child, item.mutable, preserve, item.sentinel)
case .Field:
if enum_type, enum_ok := enum_type_from_field_expr(checker, expr, pkg, file); enum_ok {
_, member_ok := find_enum_member(checker, enum_type, expr.name)
return enum_type if member_ok else types.INVALID
}
value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
if !symbol.is_valid(expr.name) {
return field_type_from_value(checker, expr, value)
}
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)
}
slot, field, ok := find_struct_field_slot(checker, value, expr.name)
if ok && is_inferred_record_field(checker, slot) && is_runtime_type(checker, expected) {
_ = merge_record_field_demand(checker, slot, expected, expr.span)
field = checker.module.types.fields[slot]
}
return field.type if ok else types.INVALID
case .Unwrap:
value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
return types.child_type(value, store) if types.is_optional(value, store) else types.INVALID
case .Orelse:
value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
_ = infer_nested_expr(checker, expr.right, locals, pkg, file, demanded, local_types)
return types.child_type(value, store) if types.is_optional(value, store) else types.INVALID
case .Try:
left_expected := expected if checker.ast_module.exprs[expr.left].kind == .Call else types.INVALID
value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types, left_expected)
merge_inferred_test_error(checker, types.fallible_error(value, store))
return types.fallible_success(value, store) if types.kind(value, store) == .Fallible else types.INVALID
case .Catch:
left_expected := expected if checker.ast_module.exprs[expr.left].kind == .Call else types.INVALID
value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types, left_expected)
success := types.fallible_success(value, store)
error_type := types.fallible_error(value, store)
fallback_locals: [dynamic]Infer_Local
fallback_locals.allocator = checker.allocator
defer delete(fallback_locals)
append(&fallback_locals, ..locals)
if symbol.is_valid(expr.name) && expr.name != checker.sink_symbol && types.is_valid(error_type) {
append(&fallback_locals, Infer_Local{name=expr.name, type=error_type, declared=error_type, statement=ast.INVALID_STMT})
}
if expr.right != ast.INVALID_EXPR {
fallback := infer_nested_expr(checker, expr.right, fallback_locals[:], pkg, file, demanded, local_types)
return success if types.is_valid(success) else fallback
}
infer_statements(checker, expr.body, &fallback_locals, local_types, pkg, file, demanded, &success, success)
return success
case .Struct_Literal:
value := types.INVALID
if expr.left != ast.INVALID_EXPR {
value, _ = resolve_type_argument(checker, expr.left, pkg, file)
} else if symbol.is_valid(expr.name) {
target_pkg, available := expr_package(checker, expr, pkg, file)
value = types.find_named(store, u32(target_pkg), u32(expr.name), file=u32(expr_lookup_file(expr, file))) if available else types.INVALID
} else {
value = expected
}
value = types.resolve_alias(value, store)
tuple_literal := expr.tuple
if tuple_literal && len(expr.args) == 0 && types.is_valid(value) {
item, ok := types.node(store, value)
tuple_literal = !ok || item.kind != .Struct || item.tuple
}
if tuple_literal {
if !types.is_valid(value) {
fields := make([]types.Field, len(expr.args), checker.allocator)
defer delete(fields, checker.allocator)
for arg, index in expr.args {
fields[index].type = infer_nested_expr(checker, arg, locals, pkg, file, demanded, local_types)
}
return types.struct_anonymous(store, fields, true)
}
item, ok := types.node(store, value)
if !ok || item.kind != .Struct || !item.tuple {
for arg in expr.args {
_ = infer_nested_expr(checker, arg, locals, pkg, file, demanded, local_types)
}
return types.INVALID
}
fields := types.fields_for(store, value)
for arg, index in expr.args {
expected_field := fields[index].type if index < len(fields) else types.INVALID
_ = infer_nested_expr(checker, arg, locals, pkg, file, demanded, local_types, expected_field)
}
return value
}
if !types.is_valid(value) {
fields := make([]types.Field, len(expr.args), checker.allocator)
defer delete(fields, checker.allocator)
valid := true
for keyed, index in expr.args {
keyed_expr := checker.ast_module.exprs[keyed]
fields[index].name = u32(keyed_expr.name)
if keyed_expr.left == ast.INVALID_EXPR {
valid = false
continue
}
for previous in fields[:index] {
valid = valid && previous.name != fields[index].name
}
fields[index].type = infer_nested_expr(
checker, keyed_expr.left, locals, pkg, file, demanded, local_types,
)
}
return types.struct_anonymous(store, fields) if valid else types.INVALID
}
fields := types.fields_for(store, value)
item, item_ok := types.node(store, value)
initialized := make([]bool, len(fields), checker.allocator)
defer delete(initialized, checker.allocator)
for keyed in expr.args {
keyed_expr := checker.ast_module.exprs[keyed]
if keyed_expr.left == ast.INVALID_EXPR {
continue
}
slot, field, ok := find_struct_field_slot(checker, value, keyed_expr.name)
if ok && item_ok {
initialized[slot-int(item.field_start)] = true
}
if !ok {
_ = infer_nested_expr(checker, keyed_expr.left, locals, pkg, file, demanded, local_types)
continue
}
field_expected := field.type if is_runtime_type(checker, field.type) else types.INVALID
actual := infer_nested_expr(checker, keyed_expr.left, locals, pkg, file, demanded, local_types, field_expected)
_ = record_demand(
checker, keyed_expr.left, field_expected, locals, local_types, pkg, file,
)
if !is_inferred_record_field(checker, slot) {
continue
}
_ = record_field_expr_candidate(checker, slot, keyed_expr.left, actual, locals, pkg, file)
if is_runtime_type(checker, checker.module.types.fields[slot].type) {
_ = record_demand(
checker, keyed_expr.left, checker.module.types.fields[slot].type,
locals, local_types, pkg, file,
)
}
}
if item_ok && item.kind == .Struct && !item.tuple {
for field, index in fields {
if initialized[index] {
continue
}
if field_default, default_values, ok := find_struct_field_default(checker, value, symbol.Id(field.name)); ok {
if field_default.static_value != INVALID_CT_VALUE {
continue
}
previous := checker.current_comptime_values
checker.current_comptime_values = default_values
_ = infer_nested_expr(
checker, field_default.expr, nil, field_default.pkg, field_default.file,
demanded, expected=field.type,
)
checker.current_comptime_values = previous
}
}
}
return value
case .Keyed:
return infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
case:
return types.INVALID
}
}
infer_expr :: proc(
checker: ^Checker,
expr_id: ast.Expr_Id,
locals: []Infer_Local,
pkg := ast.Package_Id(0),
file := ast.File_Id(0),
demanded: ^[dynamic]Spec_Id = nil,
local_types: []types.Type = nil,
expected := types.INVALID,
) -> types.Type {
stack := checker.infer_stack
checker.infer_stack = nil
clear_dynamic_array(&stack)
defer {
for frame in stack {
delete(frame.args, checker.allocator)
delete(frame.mapping, checker.allocator)
}
clear_dynamic_array(&stack)
if checker.infer_stack == nil {
checker.infer_stack = stack
} else {
delete(stack)
}
}
append(&stack, Infer_Frame{expr=expr_id, expected=expected, template=ast.INVALID_FUNCTION})
last := types.INVALID
for len(stack) > 0 {
frame_index := len(stack)-1
frame := stack[frame_index]
if frame.expr == ast.INVALID_EXPR || int(frame.expr) >= len(checker.ast_module.exprs) {
last = types.INVALID
_ = pop(&stack)
continue
}
expr := checker.ast_module.exprs[frame.expr]
if frame.stage == 0 {
constant := Constant{}
_, static_name := current_static_binding(checker, expr.name)
if expr.kind != .Name || symbol.is_valid(expr.qualifier) || !static_name {
constant = eval_constant(checker, frame.expr)
}
if constant.kind == .Overflow || constant.kind == .Div_By_Zero {
last = types.I64
_ = pop(&stack)
continue
}
if constant.kind == .Value {
if frame.numeric_operation {
if representation, ok := types.distinct_scalar_backing(frame.expected, &checker.module.types);
ok && (types.is_float(representation, checker.target) ||
types.is_concrete_integer(representation) &&
fits_integer_type(constant.value, representation, checker.target)) {
last = frame.expected
_ = pop(&stack)
continue
}
}
last = constraint_integer_literal_type(frame.expected, constant.value)
if !types.is_valid(last) {
last = types.I64
}
_ = pop(&stack)
continue
}
switch expr.kind {
case .Invalid:
last = types.INVALID
_ = pop(&stack)
case .Inference_Hole:
last = types.INVALID
_ = pop(&stack)
case .Type:
last = types.INVALID
_ = pop(&stack)
case .Anonymous_Struct_Type:
last = types.INVALID
_ = pop(&stack)
case .Integer:
last = types.I64
if expr.integer <= 0x7fff_ffff_ffff_ffff {
last = types.smallest_signed_for_literal(i64(expr.integer))
}
_ = pop(&stack)
case .Float:
if frame.numeric_operation {
if representation, ok := types.distinct_scalar_backing(frame.expected, &checker.module.types);
ok && types.is_float(representation, checker.target) {
last = frame.expected
_ = pop(&stack)
continue
}
}
last = frame.expected if types.is_float(frame.expected, checker.target) else types.F64
_ = pop(&stack)
case .String, .Array, .Null, .Unreachable, .Undefined, .Address, .Deref, .Index, .Slice,
.Field, .Unwrap, .Orelse, .Try, .Catch, .Struct_Literal, .Keyed, .Enum_Literal, .Cast,
.Comptime, .Bool, .Not, .Bit_Not, .Bit_And, .Bit_Or, .Bit_Xor, .Shift_Left,
.Shift_Right, .Shift_Left_Saturating, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range:
last = infer_compound_expr(checker, expr, locals, pkg, file, demanded, local_types, frame.expected)
_ = pop(&stack)
case .Function_Literal:
template := ast.Function_Id(u32(expr.integer))
function_type, ok := function_expr_type_for_template(checker, template, frame.expected, demanded)
last = function_type if ok else types.INVALID
_ = pop(&stack)
case .Name:
last = types.INVALID
if !symbol.is_valid(expr.qualifier) {
last = find_infer_local(locals, expr.name)
} else {
base_type := find_infer_local(locals, expr.qualifier)
item, has_item := types.container(base_type, &checker.module.types)
field_name := symbol_text(checker, expr.name)
if has_item && (item.kind == .Array || item.kind == .Slice) {
if field_name == "len" {
last = types.USIZE
} else if field_name == "ptr" &&
(item.kind == .Slice || types.is_pointer(base_type, &checker.module.types)) {
last = container_pointer_type(&checker.module.types, item)
}
}
if types.is_pointer(base_type, &checker.module.types) {
base_type = types.child_type(base_type, &checker.module.types)
}
if !types.is_valid(last) {
slot, field, ok := find_struct_field_slot(checker, base_type, expr.name)
if ok {
if is_inferred_record_field(checker, slot) && is_runtime_type(checker, frame.expected) {
_ = merge_record_field_demand(checker, slot, frame.expected, expr.span)
field = checker.module.types.fields[slot]
}
last = field.type
}
}
}
if !types.is_valid(last) && symbol.is_valid(expr.qualifier) &&
find_import(checker, file, expr.qualifier) == ast.INVALID_IMPORT {
if value, ok := current_comptime_value(checker, expr.qualifier);
ok && value.kind == .Static {
if _, field, found := find_struct_field(checker, value.type, expr.name); found {
last = field.type
}
}
if value, ok := current_static_binding(checker, expr.qualifier); ok {
if _, field, found := find_struct_field(checker, value.type, expr.name); found {
last = field.type
}
}
if global := find_global(checker, expr.qualifier, pkg, file); global != ast.INVALID_GLOBAL {
base_type := checker.global_types[global]
item, has_item := types.container(base_type, &checker.module.types)
field_name := symbol_text(checker, expr.name)
if has_item && (item.kind == .Array || item.kind == .Slice) {
if field_name == "len" {
last = types.USIZE
} else if field_name == "ptr" &&
(item.kind == .Slice || types.is_pointer(base_type, &checker.module.types)) {
last = container_pointer_type(&checker.module.types, item)
}
}
if types.is_pointer(base_type, &checker.module.types) {
base_type = types.child_type(base_type, &checker.module.types)
}
if !types.is_valid(last) {
slot, field, ok := find_struct_field_slot(checker, base_type, expr.name)
if ok {
if is_inferred_record_field(checker, slot) && is_runtime_type(checker, frame.expected) {
_ = merge_record_field_demand(checker, slot, frame.expected, expr.span)
field = checker.module.types.fields[slot]
}
last = field.type
}
}
}
}
if !types.is_valid(last) {
if !symbol.is_valid(expr.qualifier) {
if value, ok := current_comptime_value(checker, expr.name); ok {
if value.kind == .Integer || value.kind == .String || value.kind == .Static {
last = value.type
}
}
if binding, ok := current_static_binding(checker, expr.name); ok {
last = binding.type
}
}
}
if !types.is_valid(last) {
if enum_type, enum_ok := enum_type_from_name_expr(checker, expr, pkg, file); enum_ok {
_, member_ok := find_enum_member(checker, enum_type, expr.name)
if member_ok {
last = enum_type
}
}
}
if !types.is_valid(last) {
target_pkg, available := expr_package(checker, expr, pkg, file)
if available {
global := find_global(checker, expr.name, target_pkg, expr_lookup_file(expr, file))
if global != ast.INVALID_GLOBAL {
last = checker.global_types[global]
}
}
}
if !types.is_valid(last) {
target_pkg, available := expr_package(checker, expr, pkg, file)
if available {
template := find_template(checker, expr.name, target_pkg, expr_lookup_file(expr, file))
if template != ast.INVALID_FUNCTION &&
len(checker.ast_module.functions[template].unsupported_reason) == 0 &&
checker.template_diagnostics[template] == source.INVALID_DIAGNOSTIC {
function_type, ok := function_expr_type_for_template(checker, template, frame.expected, demanded)
if ok {
last = function_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
left_const := is_numeric_constant_expr(checker, expr.left)
right_const := is_numeric_constant_expr(checker, expr.right)
stack[frame_index].reverse_operands = left_const && !right_const
first := expr.right if stack[frame_index].reverse_operands else expr.left
first_expected := frame.expected if
types.is_concrete_scalar(frame.expected) && !types.is_bool(frame.expected) else types.INVALID
append(&stack, Infer_Frame{
expr=first,
expected=first_expected,
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.callable_function(callee_type, &checker.module.types)
if !ok {
last = types.INVALID
_ = pop(&stack)
continue
}
stack[frame_index].left = function_type
stack[frame_index].args = make([]types.Type, len(expr.args), checker.allocator)
stack[frame_index].stage = 6
if len(expr.args) > 0 {
append(&stack, Infer_Frame{expr=expr.args[0], template=ast.INVALID_FUNCTION})
} else if valid_callable_arity(function_item, 0) {
last = function_item.child
delete(stack[frame_index].args, checker.allocator)
stack[frame_index].args = nil
_ = pop(&stack)
}
continue
}
if builtin := type_builtin_call(checker, expr); builtin != .None {
if builtin == .Size_Of || builtin == .Align_Of {
last = types.USIZE
} else if len(expr.args) == 1 {
target, ok := resolve_type_argument(checker, expr.args[0], pkg, file)
last = target if ok && is_integer_bound_type(checker, target) else types.INVALID
} else {
last = types.INVALID
}
_ = pop(&stack)
continue
}
if division_builtin_call(checker, expr) != .None {
last = infer_division_builtin(checker, expr, locals, pkg, file, demanded, local_types, frame.expected)
_ = pop(&stack)
continue
}
if builtin := memory_builtin_call(checker, expr); builtin != .None {
last = infer_memory_builtin(checker, expr, builtin, locals, pkg, file, demanded, local_types)
_ = pop(&stack)
continue
}
if is_intrinsic_call(checker, expr, "some") {
if len(expr.args) == 1 && types.is_optional(frame.expected, &checker.module.types) {
child := types.child_type(frame.expected, &checker.module.types)
_ = infer_nested_expr(checker, expr.args[0], locals, pkg, file, demanded, local_types, child)
last = frame.expected
} else {
last = types.INVALID
}
_ = pop(&stack)
continue
}
if field_expr, handled := field_intrinsic_expr(checker, expr, pkg, file); handled {
if enum_type, ok := resolve_type_argument(checker, field_expr.left, pkg, file);
ok && types.is_enum(enum_type, &checker.module.types) {
_, member_ok := find_enum_member(checker, enum_type, field_expr.name)
last = enum_type if member_ok else types.INVALID
_ = pop(&stack)
continue
}
base_type := infer_nested_expr(
checker, field_expr.left, locals, pkg, file, demanded, local_types,
)
last = field_type_from_value(checker, field_expr, base_type)
_ = pop(&stack)
continue
}
if is_intrinsic_call(checker, expr, "typeinfo") {
_, target_ok := resolve_type_argument(
checker, expr.args[0] if len(expr.args) == 1 else ast.INVALID_EXPR, pkg, file,
)
last = std_named_type(checker, "@std/meta", "TypeInfo") if target_ok else types.INVALID
_ = pop(&stack)
continue
}
if is_intrinsic_call(checker, expr, "tag") {
if len(expr.args) == 1 {
value_type := infer_nested_expr(checker, expr.args[0], locals, pkg, file, demanded, local_types)
last, _ = tag_result_type(checker, value_type)
} else {
last = types.INVALID
}
_ = pop(&stack)
continue
}
if is_intrinsic_call(checker, expr, "tagname") {
last = types.slice(&checker.module.types, types.U8, false)
_ = pop(&stack)
continue
}
if is_ptrcast_call(checker, expr) {
if len(expr.args) != 2 {
last = types.INVALID
_ = pop(&stack)
continue
}
child, child_ok := resolve_type_argument(checker, expr.args[0], pkg, file)
operand := infer_nested_expr(checker, expr.args[1], locals, pkg, file, demanded, local_types)
result := types.INVALID
if child_ok && valid_ptrcast_child(checker, child) {
result, _ = types.replace_pointer_child(&checker.module.types, operand, child)
}
last = result
_ = pop(&stack)
continue
}
if is_intrinsic_call(checker, expr, "constcast") {
if len(expr.args) == 1 {
operand := infer_nested_expr(checker, expr.args[0], locals, pkg, file, demanded, local_types)
last, _ = types.restore_mutability(&checker.module.types, operand)
} else {
last = types.INVALID
}
_ = pop(&stack)
continue
}
if is_intrinsic_call(checker, expr, "compile_error") {
last = types.VOID
_ = pop(&stack)
continue
}
if expr.intrinsic {
last = types.INVALID
_ = pop(&stack)
continue
}
if callee_type, handled := infer_qualified_value_field_type(checker, expr, locals, pkg, file, demanded); handled {
_, function_item, function_type, ok := types.callable_function(callee_type, &checker.module.types)
if !ok {
last = types.INVALID
_ = pop(&stack)
continue
}
stack[frame_index].left = function_type
stack[frame_index].args = make([]types.Type, len(expr.args), checker.allocator)
stack[frame_index].stage = 6
if len(expr.args) > 0 {
append(&stack, Infer_Frame{expr=expr.args[0], template=ast.INVALID_FUNCTION})
} else if valid_callable_arity(function_item, 0) {
last = function_item.child
delete(stack[frame_index].args, checker.allocator)
stack[frame_index].args = nil
_ = pop(&stack)
}
continue
}
target_pkg, available := expr_package(checker, expr, pkg, file)
template := ast.INVALID_FUNCTION
if available {
template = find_template(checker, expr.name, target_pkg, expr_lookup_file(expr, file))
}
if template == ast.INVALID_FUNCTION {
callee_type := types.INVALID
if !symbol.is_valid(expr.qualifier) {
callee_type = find_infer_local(locals, expr.name)
if !types.is_valid(callee_type) {
if binding, ok := current_static_binding(checker, expr.name); ok {
callee_type = binding.type
if binding.value != INVALID_CT_VALUE && int(binding.value) < len(checker.static_state.values) {
function_value := checker.static_state.values[binding.value]
if function_value.kind == .Function {
_, _, _ = function_pointer_type_for_template(
checker, ast.Function_Id(u32(function_value.index)), demanded,
)
}
}
}
}
if !types.is_valid(callee_type) {
if value, ok := current_comptime_value(checker, expr.name); ok && value.kind == .Static {
callee_type = value.type
if value.static_value != INVALID_CT_VALUE && int(value.static_value) < len(checker.static_state.values) {
function_value := checker.static_state.values[value.static_value]
if function_value.kind == .Function {
_, _, _ = function_pointer_type_for_template(
checker, ast.Function_Id(u32(function_value.index)), demanded,
)
}
}
}
}
}
if !types.is_valid(callee_type) && available {
global := find_global(checker, expr.name, target_pkg, expr_lookup_file(expr, file))
if global != ast.INVALID_GLOBAL {
callee_type = checker.global_types[global]
}
}
_, function_item, function_type, ok := types.callable_function(callee_type, &checker.module.types)
if !ok {
named_type := types.find_named(&checker.module.types, u32(target_pkg), u32(expr.name), file=u32(expr_lookup_file(expr, file)))
named_item, named_ok := types.node(&checker.module.types, named_type)
constructor_type := types.resolve_alias(named_type, &checker.module.types)
constructor_item, constructor_ok := types.node(&checker.module.types, constructor_type)
scalar_alias := named_ok && named_item.kind == .Alias &&
types.is_concrete_scalar(constructor_type) && !types.is_bool(constructor_type)
distinct_constructor := constructor_ok && constructor_item.kind == .Distinct
if available && len(expr.args) == 1 && (scalar_alias || distinct_constructor) {
stack[frame_index].left = constructor_type
stack[frame_index].stage = 7
append(&stack, Infer_Frame{expr=expr.args[0], template=ast.INVALID_FUNCTION})
continue
}
last = types.INVALID
_ = pop(&stack)
continue
}
stack[frame_index].left = function_type
stack[frame_index].args = make([]types.Type, len(expr.args), checker.allocator)
stack[frame_index].stage = 6
if len(expr.args) > 0 {
append(&stack, Infer_Frame{expr=expr.args[0], template=ast.INVALID_FUNCTION})
} else if valid_callable_arity(function_item, 0) {
last = function_item.child
delete(stack[frame_index].args, checker.allocator)
stack[frame_index].args = nil
_ = pop(&stack)
}
continue
}
if len(checker.ast_module.functions[template].unsupported_reason) > 0 {
last = types.INVALID
_ = pop(&stack)
continue
}
if checker.template_diagnostics[template] != source.INVALID_DIAGNOSTIC {
declared := function_channel_type(checker, checker.ast_module.functions[template])
last = declared if is_runtime_type(checker, declared) || types.is_void(declared) || types.is_noreturn(declared) else types.INVALID
_ = pop(&stack)
continue
}
function := checker.ast_module.functions[template]
mapping, mode, prefix, mapping_failure := call_argument_mapping(
checker, &function, expr.args, pkg, file, frame.expected, locals,
)
delete(mapping_failure, checker.allocator)
if mode == .Invalid {
last = types.INVALID
_ = pop(&stack)
continue
}
stack[frame_index].template = template
stack[frame_index].arg_mode = mode
stack[frame_index].prefix = prefix
stack[frame_index].mapping = mapping
stack[frame_index].args = make([]types.Type, len(function.params), checker.allocator)
stack[frame_index].arg_index = next_runtime_call_arg(function, mapping, 0, len(expr.args))
stack[frame_index].stage = 3
if stack[frame_index].arg_index < len(expr.args) {
param_index := call_param_index(mapping, stack[frame_index].arg_index)
arg_expected := call_arg_expected(checker, function, param_index)
if !is_runtime_type(checker, arg_expected) {
arg_expected = types.INVALID
}
append(&stack, Infer_Frame{expr=expr.args[stack[frame_index].arg_index], expected=arg_expected, template=ast.INVALID_FUNCTION})
}
}
continue
}
if frame.stage == 5 {
if !types.is_signed(last, checker.target) && !types.is_float(last, checker.target) {
last = types.INVALID
}
_ = pop(&stack)
continue
}
if frame.stage == 1 {
stack[frame_index].left = last
stack[frame_index].stage = 2
second := expr.left if frame.reverse_operands else expr.right
second_expected := types.INVALID
numeric_operation := false
if is_numeric_constant_expr(checker, second) {
second_expected = last
if representation, ok := types.distinct_scalar_backing(last, &checker.module.types);
ok && !types.is_bool(representation) {
numeric_operation = true
}
}
append(&stack, Infer_Frame{
expr=second,
expected=second_expected,
template=ast.INVALID_FUNCTION,
numeric_operation=numeric_operation,
})
continue
}
if frame.stage == 2 {
first, second := frame.left, last
left, right := first, second
if frame.reverse_operands {
left, right = second, first
}
if expr.kind == .Add && types.is_many_pointer(left, &checker.module.types) && types.is_concrete_integer(right) {
last = left
} else if operation, ok := numeric_operation_type(checker, left, right); ok {
last = operation.result
} else if is_numeric_constant_expr(checker, expr.right) &&
is_numeric_demand(left, checker.target) &&
expr_accepts_numeric_demand(checker, expr.right, left, locals, pkg, file) {
last = 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(left, checker.target) &&
!numeric_operand_is_open(checker, expr.left, locals, pkg, file) &&
expr_accepts_numeric_demand(checker, expr.right, left, locals, pkg, file) {
_ = record_demand(checker, expr.right, left, locals, local_types, pkg, file)
last = 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.INVALID
}
_ = pop(&stack)
continue
}
if frame.stage == 3 {
if frame.arg_index < len(expr.args) {
param_index := call_param_index(frame.mapping, frame.arg_index)
if param_index < len(stack[frame_index].args) {
stack[frame_index].args[param_index] = last
}
next := next_runtime_call_arg(
checker.ast_module.functions[frame.template], frame.mapping,
frame.arg_index+1, len(expr.args),
)
stack[frame_index].arg_index = next
if next < len(expr.args) {
next_param := call_param_index(frame.mapping, next)
arg_expected := call_arg_expected(checker, checker.ast_module.functions[frame.template], next_param)
if !is_runtime_type(checker, arg_expected) {
arg_expected = types.INVALID
}
append(&stack, Infer_Frame{expr=expr.args[next], expected=arg_expected, template=ast.INVALID_FUNCTION})
continue
}
}
function := checker.ast_module.functions[frame.template]
// Deferred defaulting leaves an undemanded open constant typeless; give such an
// argument its default so the call can still monomorphize (the default feeds only
// the spec arg vector, not a demand).
for source_index in 0..<len(expr.args) {
param_index := call_param_index(frame.mapping, source_index)
if param_index < len(stack[frame_index].args) && !is_runtime_type(checker, stack[frame_index].args[param_index]) {
fallback := open_const_default_type(checker, expr.args[source_index], locals, pkg, file)
if is_runtime_type(checker, fallback) {
stack[frame_index].args[param_index] = fallback
}
}
}
comptime_values: []Comptime_Value
comptime_ok := false
comptime_values, comptime_ok = infer_call_comptime_values(
checker, function, frame.prefix, frame.mapping, expr.args, stack[frame_index].args,
frame.expected, pkg, file,
)
defer delete(comptime_values, checker.allocator)
if comptime_ok &&
can_specialize(checker, function, stack[frame_index].args, comptime_values) {
resolution := store_call_resolution(
checker, frame.expr, frame.mapping,
comptime_values, stack[frame_index].args,
)
fold := Call_Fold.Runtime
if can_fold_zero_runtime_call(checker, function) {
fold = cache_zero_runtime_call(checker, resolution, frame.expr, pkg, file)
}
if fold == .Compile_Error {
last = types.INVALID
} else if fold == .Value {
// Folded specializations are still inferred to validate their bodies and
// participate in the type-demand fixpoint. Leaving them undemanded keeps
// prune_specs from emitting an otherwise unused runtime function.
if !checker.building_hir && demanded == nil && !function_has_comptime_params(function) {
_ = ensure_spec(checker, frame.template, stack[frame_index].args, comptime_values)
}
value := checker.call_resolutions[resolution].folded_value
last = checker.static_state.values[value].type
} else {
previous_comptime := checker.current_comptime_values
checker.current_comptime_values = comptime_values
for source_index in 0..<len(expr.args) {
param_index := call_param_index(frame.mapping, source_index)
if param_index >= len(function.params) || function.params[param_index].comptime_value {
continue
}
demand := call_arg_expected(checker, function, param_index)
record_demand(checker, expr.args[source_index], demand, locals, local_types, pkg, file)
}
checker.current_comptime_values = previous_comptime
spec := INVALID_SPEC
if demanded == nil {
spec = ensure_spec(checker, frame.template, stack[frame_index].args, comptime_values)
} else {
spec = find_spec(checker, frame.template, stack[frame_index].args, comptime_values)
if spec == INVALID_SPEC {
spec = ensure_spec(checker, frame.template, stack[frame_index].args, comptime_values)
}
mark_spec_demanded(checker, spec, demanded)
}
if spec != INVALID_SPEC {
last = checker.specs[spec].result
} else {
previous_comptime = checker.current_comptime_values
checker.current_comptime_values = comptime_values
declared := function_channel_type(checker, function)
checker.current_comptime_values = previous_comptime
last = declared if is_runtime_type(checker, declared) || types.is_void(declared) || types.is_noreturn(declared) else types.INVALID
}
}
} else {
declared := function_channel_type(checker, function)
last = declared if is_runtime_type(checker, declared) || types.is_void(declared) || types.is_noreturn(declared) else types.INVALID
}
delete(stack[frame_index].args, checker.allocator)
stack[frame_index].args = nil
delete(stack[frame_index].mapping, checker.allocator)
stack[frame_index].mapping = nil
_ = pop(&stack)
}
if frame.stage == 6 {
if frame.arg_index < len(expr.args) {
stack[frame_index].args[frame.arg_index] = last
stack[frame_index].arg_index += 1
if frame.arg_index+1 < len(expr.args) {
append(&stack, Infer_Frame{expr=expr.args[frame.arg_index+1], template=ast.INVALID_FUNCTION})
continue
}
}
function_item, ok := types.node(&checker.module.types, frame.left)
if ok && function_item.kind == .Function && valid_callable_arity(function_item, len(expr.args)) {
last = function_item.child
} else {
last = types.INVALID
}
delete(stack[frame_index].args, checker.allocator)
stack[frame_index].args = nil
_ = pop(&stack)
}
if frame.stage == 7 {
last = frame.left
_ = pop(&stack)
}
}
return last
}
flatten_conditional_unwrap_operands :: proc(
module: ^ast.Module,
expr_id: ast.Expr_Id,
operands: ^[dynamic]ast.Expr_Id,
) {
if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(module.exprs) {
append(operands, expr_id)
return
}
expr := module.exprs[expr_id]
if expr.kind == .And {
flatten_conditional_unwrap_operands(module, expr.left, operands)
flatten_conditional_unwrap_operands(module, expr.right, operands)
return
}
append(operands, expr_id)
}
record_infer_local_type :: proc(local: Infer_Local, local_types: []types.Type) {
if local.statement != ast.INVALID_STMT && int(local.statement) < len(local_types) {
local_types[local.statement] = local.type
}
}
merge_infer_local_type :: proc(
checker: ^Checker,
local: ^Infer_Local,
inferred: types.Type,
local_types: []types.Type,
) -> bool {
if !is_runtime_type(checker, inferred) {
return false
}
if types.is_constraint(local.declared) {
if !types.constraint_accepts(local.declared, inferred, &checker.module.types) {
return false
}
if !is_runtime_type(checker, local.type) {
local.type = inferred
record_infer_local_type(local^, local_types)
return true
}
if types.equal(local.type, inferred) {
return false
}
merged := types.widest(local.type, inferred)
if types.constraint_accepts(local.declared, merged, &checker.module.types) {
local.type = merged
record_infer_local_type(local^, local_types)
return true
}
return false
}
if is_runtime_type(checker, local.declared) {
local.type = local.declared
record_infer_local_type(local^, local_types)
return false
}
if !is_runtime_type(checker, local.type) {
local.type = inferred
record_infer_local_type(local^, local_types)
return true
}
if types.equal(local.type, inferred) {
return false
}
merged := types.widest(local.type, inferred)
if types.is_concrete_scalar(merged) {
local.type = merged
record_infer_local_type(local^, local_types)
return true
}
return false
}
infer_statements :: proc(
checker: ^Checker,
statements: []ast.Stmt_Id,
locals: ^[dynamic]Infer_Local,
local_types: []types.Type,
pkg: ast.Package_Id,
file: ast.File_Id,
demanded: ^[dynamic]Spec_Id,
result: ^types.Type,
result_hint := types.INVALID,
) {
scope_start := len(locals^)
for statement_id in statements {
statement := checker.ast_module.statements[statement_id]
#partial switch statement.kind {
case .Declaration:
if statement.expr == ast.INVALID_EXPR {
// Value block (`x :: { ... yield v }` / `x T := { ... }`): register the
// binding (its declared type when annotated, else left open) and walk
// the block body. The build pass resolves the yielded value's type
// independently — value blocks don't join the demand fixpoint.
declared_block := type_from_syntax(checker, statement.type, pkg, file)
block_type := declared_block if is_runtime_type(checker, declared_block) else types.INVALID
local := Infer_Local{
name=statement.name, type=block_type, declared=declared_block,
statement=statement_id, mutable=!statement.immutable, open_value_source=true,
}
append(locals, local)
record_infer_local_type(local, local_types)
infer_statements(checker, statement.body, locals, local_types, pkg, file, demanded, result, result_hint)
continue
}
declared_local := resolve_inferred_array(checker, type_from_syntax(checker, statement.type, pkg, file), statement.expr)
value_type := types.INVALID
if !is_undefined_expr(checker, statement.expr) {
expected := declared_local if is_runtime_type(checker, declared_local) || declared_local == types.UINT else types.INVALID
value_type = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types, expected)
declared_local = resolve_inferred_array_from_type(checker, declared_local, value_type)
}
if is_runtime_type(checker, declared_local) && !has_inferred_array_count(checker, declared_local) {
value_type = declared_local
} else if types.is_constraint(declared_local) {
// Seed the binding in-family (INVALID on mismatch, which
// build_block reports). FLOAT defaults integers to f64.
value_type = types.constraint_target(declared_local, value_type, &checker.module.types)
}
open := false
open_float := false
const_val := i128(0)
if !is_runtime_type(checker, declared_local) && !is_undefined_expr(checker, statement.expr) {
constant := eval_integer_constant_in_context(checker, statement.expr, pkg, file)
if constant.kind == .Value &&
(fits_i64(constant.value) || declared_local == types.UINT && fits_u64(constant.value)) {
open = true
const_val = constant.value
} else if is_float_constant_expr(checker, statement.expr) {
open_float = true
}
}
local := Infer_Local{
name=statement.name,
type=value_type,
declared=declared_local,
statement=statement_id,
mutable=!statement.immutable,
open_const=open,
open_float=open_float,
const_value=const_val,
}
append(locals, local)
record_infer_local_type(local, local_types)
// A typed/constraint declaration initialized by a bare name pushes its resolved
// type backward onto that name (mirrors the `Y int :: X; Z i32 :: Y` global chain).
// Skip this for a constraint/untyped declaration whose initializer is arithmetic:
// value_type is then only a provisional default (e.g. `x int = a - b` resolving to
// i16 before the operands' real uses are seen) and would poison the open-constant
// operands' family. The operands resolve from their own authoritative uses, and the
// local adopts their resolved type forward. Concrete-declared arithmetic (e.g.
// `b u16 :: a + 2`) still pushes, since value_type is the concrete declared type.
init_is_arith := is_arith_kind(checker.ast_module.exprs[statement.expr].kind)
if is_runtime_type(checker, declared_local) || !init_is_arith {
record_demand(checker, statement.expr, value_type, locals^[:], local_types, pkg, file)
}
case .Assignment:
if statement.expr == ast.INVALID_EXPR {
// Value block assigned to a target: walk the block body; the build
// pass handles the target coercion.
infer_statements(checker, statement.body, locals, local_types, pkg, file, demanded, result, result_hint)
continue
}
expected_assignment := types.INVALID
if statement.target != ast.INVALID_EXPR && !symbol.is_valid(statement.name) {
expected_assignment = 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].declared == types.UINT {
expected_assignment = types.UINT
} else if global := find_global(checker, target_expr.name, pkg, file); global != ast.INVALID_GLOBAL {
ast_global := checker.ast_module.globals[global]
if type_from_syntax(checker, ast_global.type, ast_global.pkg, ast_global.file) == types.UINT {
expected_assignment = types.UINT
}
}
}
} else if statement.name != checker.sink_symbol {
if local_index, ok := find_infer_local_index(locals^[:], statement.name); ok {
expected_assignment = types.UINT if locals^[local_index].declared == types.UINT else locals^[local_index].type
} else if global := find_global(checker, statement.name, pkg, file); global != ast.INVALID_GLOBAL {
ast_global := checker.ast_module.globals[global]
declared_global := type_from_syntax(checker, ast_global.type, ast_global.pkg, ast_global.file)
expected_assignment = types.UINT if declared_global == types.UINT else checker.global_types[global]
}
}
value_type := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types, expected_assignment)
// Only push the target's type back onto a bare-name RHS (e.g. `x += speed`):
// pushing through an arithmetic RHS would feed the target's (often provisional)
// type onto open-constant operands and poison their family. Operands of an
// arithmetic RHS resolve from their own authoritative uses.
rhs_is_arith := is_arith_kind(checker.ast_module.exprs[statement.expr].kind)
if statement.target != ast.INVALID_EXPR && !symbol.is_valid(statement.name) {
target_type := expected_assignment
target_expr := checker.ast_module.exprs[statement.target]
if target_expr.kind == .Name && !symbol.is_valid(target_expr.qualifier) {
if local_index, ok := find_infer_local_index(locals^[:], target_expr.name); ok &&
locals^[local_index].mutable {
_ = merge_infer_local_type(checker, &locals^[local_index], value_type, local_types)
}
} else if target_expr.kind == .Field ||
target_expr.kind == .Name && symbol.is_valid(target_expr.qualifier) {
if slot, ok := inferred_record_field_slot_from_expr(
checker, target_expr, locals^[:], local_types, pkg, file,
); ok {
_ = record_field_expr_candidate(
checker, slot, statement.expr, value_type, locals^[:], pkg, file,
)
target_type = checker.module.types.fields[slot].type
}
}
if !rhs_is_arith {
_ = record_demand(checker, statement.expr, target_type, locals^[:], local_types, pkg, file)
}
} else if statement.name != checker.sink_symbol {
if local_index, ok := find_infer_local_index(locals^[:], statement.name); ok &&
locals^[local_index].mutable {
_ = merge_infer_local_type(checker, &locals^[local_index], value_type, local_types)
if !rhs_is_arith {
_ = record_demand(checker, statement.expr, locals^[local_index].type, locals^[:], local_types, pkg, file)
}
} else if global := find_global(checker, statement.name, pkg, file); global != ast.INVALID_GLOBAL {
_ = merge_global_demand(checker, global, value_type)
if !rhs_is_arith {
_ = record_demand(checker, statement.expr, checker.global_types[global], locals^[:], local_types, pkg, file)
}
}
}
case .Expression:
_ = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types)
case .Yield:
if statement.expr != ast.INVALID_EXPR {
_ = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types)
}
case .Return:
if statement.value_control_flow {
infer_statements(checker, statement.body, locals, local_types, pkg, file, demanded, result, result_hint)
continue
}
if statement.expr != ast.INVALID_EXPR {
returned := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types, result_hint)
if is_runtime_type(checker, result_hint) {
_ = record_demand_shallow(checker, statement.expr, result_hint, locals^[:], local_types, pkg, file)
expr := checker.ast_module.exprs[statement.expr]
if expr.kind == .Name && !symbol.is_valid(expr.qualifier) {
if local_index, ok := find_infer_local_index(locals^[:], expr.name); ok {
// Open constants can adopt the result type cross-family; other
// locals widen within family as before.
if !merge_local_demand(checker, &locals^[local_index], result_hint, local_types) {
_ = merge_infer_local_type(checker, &locals^[local_index], result_hint, local_types)
}
returned = result_hint
} else {
// `return G` for a global const: demand the result type onto it.
record_demand_shallow(checker, statement.expr, result_hint, locals^[:], local_types, pkg, file)
}
}
}
if !types.is_valid(result^) {
result^ = returned
} else if !types.equal(result^, returned) {
result^ = types.widest(result^, returned)
}
}
case .If:
if len(statement.captures) > 0 {
operands: [dynamic]ast.Expr_Id
operands.allocator = checker.allocator
flatten_conditional_unwrap_operands(checker.ast_module, statement.expr, &operands)
operand_types := make([]types.Type, len(operands), checker.allocator)
for operand, index in operands {
operand_types[index] = infer_expr(checker, operand, locals^[:], pkg, file, demanded, local_types)
}
capture_start := len(locals^)
for capture, index in statement.captures {
if capture == checker.sink_symbol {
continue
}
capture_type := types.INVALID
if index < len(operand_types) &&
types.is_optional(operand_types[index], &checker.module.types) {
capture_type = types.child_type(operand_types[index], &checker.module.types)
}
append(locals, Infer_Local{name=capture, type=capture_type, declared=capture_type, statement=ast.INVALID_STMT})
}
if statement.guard != ast.INVALID_EXPR {
_ = infer_expr(checker, statement.guard, locals^[:], pkg, file, demanded, local_types)
}
infer_statements(checker, statement.body, locals, local_types, pkg, file, demanded, result, result_hint)
resize(locals, capture_start)
infer_statements(checker, statement.else_body, locals, local_types, pkg, file, demanded, result, result_hint)
delete(operand_types, checker.allocator)
delete(operands)
} else {
_ = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types)
if selected, comptime_ok := specialization_bool(checker, statement.expr, pkg, file); comptime_ok {
selected_body := statement.body if selected else statement.else_body
infer_statements(checker, selected_body, locals, local_types, pkg, file, demanded, result, result_hint)
} else {
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:
if statement.expand {
bindings, expand_error := expand_field_bindings(checker, statement.expr, statement.name, pkg, file)
if expand_error == .None {
for binding, expand_index in bindings {
binding_start := push_expand_binding(checker, binding, statement.index_name, expand_index, statement_id)
iteration: [dynamic]ast.Stmt_Id
iteration.allocator = checker.allocator
control := flatten_expand_iteration(
checker, statement.body, pkg, file, &iteration, statement.label, nil,
)
if control != .Invalid {
infer_statements(checker, iteration[:], locals, local_types, pkg, file, demanded, result, result_hint)
}
delete(iteration)
pop_expand_binding(checker, binding_start)
if control == .Break || control == .Invalid {
break
}
}
}
delete(bindings, checker.allocator)
continue
}
iterable_type := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types)
capture_start := len(locals^)
capture_type := types.INVALID
if types.is_range(iterable_type, &checker.module.types) {
capture_type = types.child_type(iterable_type, &checker.module.types)
} else {
item, ok := sequence_item(iterable_type, &checker.module.types)
if ok {
capture_type = item.child
if statement.pointer_capture {
capture_type = types.pointer(&checker.module.types, item.child, item.mutable, false)
}
}
}
if symbol.is_valid(statement.name) {
append(locals, Infer_Local{name=statement.name, type=capture_type, declared=capture_type, statement=ast.INVALID_STMT})
}
if symbol.is_valid(statement.index_name) {
append(locals, Infer_Local{name=statement.index_name, type=types.USIZE, declared=types.USIZE, statement=ast.INVALID_STMT})
}
infer_statements(checker, statement.body, locals, local_types, pkg, file, demanded, result, result_hint)
resize(locals, capture_start)
case .Block:
infer_statements(checker, statement.body, locals, local_types, pkg, file, demanded, result, result_hint)
case .Defer:
capture_start := len(locals^)
if statement.error_only && len(statement.captures) > 0 &&
statement.captures[0] != checker.sink_symbol {
append(locals, Infer_Local{
name=statement.captures[0],
type=types.fallible_error(result^, &checker.module.types),
declared=types.fallible_error(result^, &checker.module.types),
statement=ast.INVALID_STMT,
})
}
deferred := [1]ast.Stmt_Id{statement.update}
infer_statements(checker, deferred[:], locals, local_types, pkg, file, demanded, result, result_hint)
resize(locals, capture_start)
case .Match:
if selected_body, capture, has_capture, comptime_ok := specialization_match_body(checker, statement, pkg, file); comptime_ok {
if has_capture {
append(&checker.static_bindings, capture)
}
infer_statements(checker, selected_body, locals, local_types, pkg, file, demanded, result, result_hint)
if has_capture {
_ = pop(&checker.static_bindings)
}
continue
}
// The build pass desugars `match` to an if/else chain, but inference runs first
// and must still visit the subject and arm bodies so calls there get specialized
// (e.g. `match get()`). Mirror the `.For`/unwrap-`.If` capture handling.
subject_type := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types)
is_tagged := types.is_tagged_union(subject_type, &checker.module.types)
is_enum_subject := types.is_enum(subject_type, &checker.module.types)
covered: [dynamic]symbol.Id
covered.allocator = checker.allocator
for arm_id in statement.body {
arm := checker.ast_module.statements[arm_id]
if arm.kind != .Match_Arm {
continue
}
if arm.expand && (is_tagged || is_enum_subject) {
tag_type := types.union_tag_enum(subject_type, &checker.module.types) if is_tagged else subject_type
if is_tagged {
for field in types.fields_for(&checker.module.types, subject_type) {
name := symbol.Id(field.name)
if contains_name(covered[:], name) {
continue
}
member, found := find_enum_member(checker, tag_type, name)
if !found {
continue
}
capture_start := len(locals^)
static_start := len(checker.static_bindings)
if len(arm.captures) > 1 {
_ = push_static_integer_binding(checker, arm.captures[1], tag_type, member.value)
}
if len(arm.captures) > 0 && arm.captures[0] != checker.sink_symbol {
if types.is_void(field.type) && !arm.pointer_capture {
_ = push_static_void_binding(checker, arm.captures[0])
} else {
capture_type := field.type
if arm.pointer_capture {
capture_type = types.pointer(&checker.module.types, field.type, true, false)
}
append(locals, Infer_Local{name=arm.captures[0], type=capture_type, declared=capture_type, statement=ast.INVALID_STMT})
}
}
infer_statements(checker, arm.body, locals, local_types, pkg, file, demanded, result, result_hint)
resize(locals, capture_start)
pop_static_bindings(checker, static_start)
}
} else {
for member in types.enum_members_for(&checker.module.types, subject_type) {
name := symbol.Id(member.name)
if contains_name(covered[:], name) {
continue
}
static_start := push_static_integer_binding(checker, arm.captures[0] if len(arm.captures) > 0 else symbol.INVALID, tag_type, member.value)
infer_statements(checker, arm.body, locals, local_types, pkg, file, demanded, result, result_hint)
pop_static_bindings(checker, static_start)
}
}
continue
}
for pattern in arm.patterns {
_ = infer_expr(checker, pattern, locals^[:], pkg, file, demanded, local_types)
pattern_expr := checker.ast_module.exprs[pattern]
if pattern_expr.kind == .Enum_Literal {
append(&covered, pattern_expr.name)
}
}
capture_start := len(locals^)
if len(arm.captures) > 0 && is_tagged && len(arm.patterns) > 0 {
capture := arm.captures[0]
if capture != checker.sink_symbol {
capture_type := types.INVALID
pattern := checker.ast_module.exprs[arm.patterns[0]]
if pattern.kind == .Enum_Literal {
if _, field, ok := find_struct_field(checker, subject_type, pattern.name); ok {
capture_type = field.type
if arm.pointer_capture {
// Mutability is best-effort here; the build pass finalizes
// the exact pointer type and coerces the captured value.
capture_type = types.pointer(&checker.module.types, field.type, true, false)
}
}
}
append(locals, Infer_Local{name=capture, type=capture_type, declared=capture_type, statement=ast.INVALID_STMT})
}
}
infer_statements(checker, arm.body, locals, local_types, pkg, file, demanded, result, result_hint)
resize(locals, capture_start)
}
delete(covered)
}
}
resize(locals, scope_start)
}
infer_spec_locals_and_result :: proc(
checker: ^Checker,
id: Spec_Id,
demanded: ^[dynamic]Spec_Id = nil,
) -> ([]types.Type, types.Type) {
spec := checker.specs[id]
function := checker.ast_module.functions[spec.template]
previous_comptime := checker.current_comptime_values
checker.current_comptime_values = spec.comptime_values
defer checker.current_comptime_values = previous_comptime
declared := type_from_syntax(checker, function.result, function.pkg, function.file)
if function.pkg == 0 && function.name == checker.main_symbol && function.result == types.INT {
declared = types.I32
}
result_hint := declared if is_runtime_type(checker, declared) || types.is_noreturn(declared) || declared == types.UINT else types.INVALID
locals: [dynamic]Infer_Local
locals.allocator = checker.allocator
defer delete(locals)
local_types := make([]types.Type, len(checker.ast_module.statements), checker.allocator)
runtime_index := 0
for param, index in function.params {
if param.comptime_value {
continue
}
param_type := types.INVALID
if runtime_index < len(spec.args) {
param_type = spec.args[runtime_index]
}
append(&locals, Infer_Local{name=param.name, type=param_type, declared=param_type, statement=ast.INVALID_STMT})
runtime_index += 1
}
result := types.INVALID
test_error := function.error
previous_test_error := checker.inferred_test_error
checker.inferred_test_error = &test_error if function.test else nil
infer_statements(checker, function.body, &locals, local_types, function.pkg, function.file, demanded, &result, result_hint)
checker.inferred_test_error = previous_test_error
if function.test && !types.equal(function.error, test_error) {
checker.ast_module.functions[spec.template].error = test_error
success := types.fallible_success(checker.specs[id].result, &checker.module.types)
checker.specs[id].result = types.fallible(&checker.module.types, success, test_error)
}
if types.is_constraint(declared) {
// Narrow the inferred result to the constraint's family; an out-of-family
// result (e.g. returning a non-integer from an `int` function) yields
// INVALID and is rejected downstream.
return local_types, types.constraint_target(declared, result, &checker.module.types)
}
return local_types, declared
}
infer_spec_result :: proc(checker: ^Checker, id: Spec_Id, demanded: ^[dynamic]Spec_Id = nil) -> types.Type {
local_types, result := infer_spec_locals_and_result(checker, id, demanded)
delete(local_types, checker.allocator)
return result
}
merge_inferred_type :: proc(store: ^types.Store, current: ^types.Type, inferred: types.Type) -> bool {
if !types.is_runtime_value(inferred, store) {
return false
}
if !types.is_runtime_value(current^, store) {
current^ = inferred
return true
}
if types.equal(current^, inferred) {
return false
}
merged := types.widest(current^, inferred)
if types.is_concrete_scalar(merged) && !types.equal(current^, merged) {
current^ = merged
return true
}
return false
}
open_integer_accepts_demand :: proc(checker: ^Checker, value: i128, demand: types.Type) -> bool {
if types.is_concrete_integer(demand) {
return fits_integer_type(value, demand, checker.target)
}
return types.is_float(demand, checker.target)
}
open_float_accepts_demand :: proc(checker: ^Checker, demand: types.Type) -> bool {
return types.is_float(demand, checker.target)
}
// merge_open_const_demand records a concrete numeric demand onto an open numeric
// global's slot. Integer constants may adopt integer or float demands; float
// constants may adopt float demands. Later demands only widen within the chosen family.
merge_open_const_demand :: proc(
checker: ^Checker,
slot: ^types.Type,
demand: types.Type,
int_open: bool,
float_open: bool,
value: i128,
) -> bool {
if !(int_open && open_integer_accepts_demand(checker, value, demand) ||
float_open && open_float_accepts_demand(checker, demand)) {
return false
}
if !is_runtime_type(checker, slot^) {
slot^ = demand
return true
}
if types.equal(slot^, demand) {
return false
}
merged := types.widest(slot^, demand)
if types.is_concrete_scalar(merged) && !types.equal(slot^, merged) {
slot^ = merged
return true
}
return false
}
// merge_global_demand routes a concrete demand onto a global's slot: open constants
// adopt any fitting family, other referents widen within family. Sets a dirty flag so
// a demand pushed from a function body re-triggers the inference fixpoint.
merge_global_demand :: proc(checker: ^Checker, global: ast.Global_Id, demand: types.Type) -> bool {
index := int(global)
if index < 0 || index >= len(checker.global_demands) {
return false
}
ast_global := checker.ast_module.globals[global]
declared := type_from_syntax(checker, ast_global.type, ast_global.pkg, ast_global.file)
if types.is_constraint(declared) && !types.constraint_accepts(declared, demand, &checker.module.types) {
return false
}
changed: bool
if checker.global_open_const[index] || checker.global_open_float[index] {
changed = merge_open_const_demand(
checker,
&checker.global_demands[index],
demand,
checker.global_open_const[index],
checker.global_open_float[index],
checker.global_const_value[index],
)
} else {
changed = merge_inferred_type(&checker.module.types, &checker.global_demands[index], demand)
}
checker.global_demands_dirty = checker.global_demands_dirty || changed
return changed
}
// merge_local_demand records a concrete demand onto an unresolved value source or
// open numeric constant. Numeric demands retain their literal-family restrictions.
merge_local_demand :: proc(checker: ^Checker, local: ^Infer_Local, demand: types.Type, local_types: []types.Type) -> bool {
if local.open_value_source {
return merge_infer_local_type(checker, local, demand, local_types)
}
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 constraint_integer_literal_type(local.declared, local.const_value)
}
if local.open_float {
return types.F64
}
return types.INVALID
}
}
target_pkg, available := expr_package(checker, expr, pkg, file)
if !available {
return types.INVALID
}
global := find_global(checker, expr.name, target_pkg, expr_lookup_file(expr, file))
index := int(global)
if global == ast.INVALID_GLOBAL || index < 0 || index >= len(checker.global_open_const) {
return types.INVALID
}
if checker.global_open_const[index] {
ast_global := checker.ast_module.globals[global]
declared := type_from_syntax(checker, ast_global.type, ast_global.pkg, ast_global.file)
return constraint_integer_literal_type(declared, checker.global_const_value[index])
}
if checker.global_open_float[index] {
return types.F64
}
return types.INVALID
}
// numeric_operand_is_open reports whether an arithmetic operand still carries a
// provisional type (an open constant at its smallest-signed default, or a bare
// numeric literal) rather than an authoritative one. A provisional operand must not
// propagate its type onto a sibling open constant: doing so locks the sibling into a
// default family and blocks the real backward demand from the declaration/use. Two
// provisional operands are instead resolved together by that backward demand.
numeric_operand_is_open :: proc(
checker: ^Checker,
expr_id: ast.Expr_Id,
locals: []Infer_Local,
pkg: ast.Package_Id,
file: ast.File_Id,
) -> bool {
if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) {
return false
}
expr := checker.ast_module.exprs[expr_id]
#partial switch expr.kind {
case .Name:
// A bare open-constant name is open iff it has a deferred default to assign.
return is_runtime_type(checker, open_const_default_type(checker, expr_id, locals, pkg, file))
case .Negate:
return numeric_operand_is_open(checker, expr.left, locals, pkg, file)
case .Add, .Sub, .Mul, .Div:
return numeric_operand_is_open(checker, expr.left, locals, pkg, file) ||
numeric_operand_is_open(checker, expr.right, locals, pkg, file)
}
// A bare integer/float literal adapts freely, so it too is provisional.
return is_numeric_constant_expr(checker, expr_id)
}
expr_accepts_numeric_demand :: proc(
checker: ^Checker,
expr_id: ast.Expr_Id,
demand: types.Type,
locals: []Infer_Local,
pkg: ast.Package_Id,
file: ast.File_Id,
) -> bool {
if !is_numeric_demand(demand, checker.target) ||
expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) {
return false
}
if constant := eval_integer_constant_in_context(checker, expr_id, pkg, file); constant.kind == .Value {
return open_integer_accepts_demand(checker, constant.value, demand)
}
if is_float_constant_expr(checker, expr_id) {
return open_float_accepts_demand(checker, demand)
}
expr := checker.ast_module.exprs[expr_id]
#partial switch expr.kind {
case .Name:
if !symbol.is_valid(expr.qualifier) {
if index, ok := find_infer_local_index(locals, expr.name); ok {
local := locals[index]
return local.open_const && open_integer_accepts_demand(checker, local.const_value, demand) ||
local.open_float && open_float_accepts_demand(checker, demand)
}
}
target_pkg, available := expr_package(checker, expr, pkg, file)
if !available {
return false
}
global := find_global(checker, expr.name, target_pkg, expr_lookup_file(expr, file))
index := int(global)
if global == ast.INVALID_GLOBAL || index < 0 || index >= len(checker.global_open_const) {
return false
}
return checker.global_open_const[index] &&
open_integer_accepts_demand(checker, checker.global_const_value[index], demand) ||
checker.global_open_float[index] && open_float_accepts_demand(checker, demand)
case .Negate:
if !types.is_signed(demand, checker.target) && !types.is_float(demand, checker.target) {
return false
}
return expr_accepts_numeric_demand(checker, expr.left, demand, locals, pkg, file)
case .Add, .Sub, .Mul, .Div:
return expr_accepts_numeric_demand(checker, expr.left, demand, locals, pkg, file) &&
expr_accepts_numeric_demand(checker, expr.right, demand, locals, pkg, file)
}
return false
}
DEMAND_RECURSION_LIMIT :: 4096
record_demand_too_deep :: proc(checker: ^Checker, expr_id: ast.Expr_Id) -> bool {
stack: [dynamic]ast.Expr_Id
stack.allocator = checker.allocator
defer delete(stack)
append(&stack, expr_id)
seen := 0
for len(stack) > 0 {
current := pop(&stack)
if current == ast.INVALID_EXPR || int(current) >= len(checker.ast_module.exprs) {
continue
}
seen += 1
if seen > DEMAND_RECURSION_LIMIT {
return true
}
expr := checker.ast_module.exprs[current]
#partial switch expr.kind {
case .Negate:
append(&stack, expr.left)
case .Add, .Sub, .Mul, .Div:
append(&stack, expr.left, expr.right)
}
}
return false
}
record_demand_shallow :: proc(
checker: ^Checker,
expr_id: ast.Expr_Id,
demand: types.Type,
locals: []Infer_Local,
local_types: []types.Type,
pkg: ast.Package_Id,
file: ast.File_Id,
) -> bool {
if record_demand_too_deep(checker, expr_id) {
return false
}
return record_demand(checker, expr_id, demand, locals, local_types, pkg, file)
}
inferred_record_field_slot_from_expr :: proc(
checker: ^Checker,
expr: ast.Expr,
locals: []Infer_Local,
local_types: []types.Type,
pkg: ast.Package_Id,
file: ast.File_Id,
) -> (int, bool) {
base := types.INVALID
field_name := expr.name
if expr.kind == .Field {
base = infer_nested_expr(checker, expr.left, locals, pkg, file, nil, local_types)
} else if expr.kind == .Name && symbol.is_valid(expr.qualifier) &&
find_import(checker, file, expr.qualifier) == ast.INVALID_IMPORT {
base = find_infer_local(locals, expr.qualifier)
if !types.is_valid(base) {
if global := find_global(checker, expr.qualifier, pkg, file); global != ast.INVALID_GLOBAL {
base = checker.global_types[global]
}
}
} else {
return -1, false
}
if types.is_pointer(base, &checker.module.types) {
base = types.child_type(base, &checker.module.types)
}
slot, _, ok := find_struct_field_slot(checker, base, field_name)
return slot, ok && is_inferred_record_field(checker, slot)
}
// record_demand pushes a concrete type demand onto open numeric slots reachable
// through bare names, inferred record fields, 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 slot, ok := inferred_record_field_slot_from_expr(
checker, expr, locals, local_types, pkg, file,
); ok {
return merge_record_field_demand(checker, slot, demand, expr.span)
}
}
if !symbol.is_valid(expr.qualifier) {
if index, ok := find_infer_local_index(locals, expr.name); ok {
return merge_local_demand(checker, &locals[index], demand, local_types)
}
}
target_pkg, available := expr_package(checker, expr, pkg, file)
if !available {
return false
}
global := find_global(checker, expr.name, target_pkg, expr_lookup_file(expr, file))
if global != ast.INVALID_GLOBAL {
return merge_global_demand(checker, global, demand)
}
case .Field:
if slot, ok := inferred_record_field_slot_from_expr(
checker, expr, locals, local_types, pkg, file,
); ok {
return merge_record_field_demand(checker, slot, demand, expr.span)
}
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
}
case .Call:
if division_builtin_call(checker, expr) != .None && len(expr.args) == 2 && is_numeric_demand(demand, checker.target) {
left := record_demand(checker, expr.args[0], demand, locals, local_types, pkg, file)
right := record_demand(checker, expr.args[1], 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 {
if global.diagnostic != source.INVALID_DIAGNOSTIC {
checker.global_types[index] = types.I64
continue
}
declared := resolve_inferred_array(
checker,
type_from_syntax(checker, global.type, global.pkg, global.file),
global.expr,
)
if is_runtime_type(checker, declared) && !has_inferred_array_count(checker, declared) {
checker.global_types[index] = declared
continue
}
if global.external {
continue
}
if global.expr != ast.INVALID_EXPR && int(global.expr) < len(checker.ast_module.exprs) {
expr := checker.ast_module.exprs[global.expr]
if builtin := type_builtin_call(checker, expr); builtin != .None {
if builtin == .Size_Of || builtin == .Align_Of {
checker.global_types[index] = types.USIZE
} else if len(expr.args) == 1 {
target, ok := resolve_type_argument(checker, expr.args[0], global.pkg, global.file)
if ok && is_integer_bound_type(checker, target) {
checker.global_types[index] = target
}
}
continue
}
}
constant := eval_integer_constant_in_context(checker, global.expr, global.pkg, global.file)
if constant.kind == .Value &&
(fits_i64(constant.value) || declared == types.UINT && fits_u64(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
}
}
for function, index in checker.ast_module.functions {
if function.analysis_root {
ensure_spec(checker, ast.function_id(index), nil)
}
}
main_template := find_template(checker, checker.main_symbol, 0)
if main_template != ast.INVALID_FUNCTION {
ensure_spec(checker, main_template, nil)
}
if checker.entry_point == .Process {
ensure_spec(checker, checker.io_provider_template, nil)
}
defaults_applied := false
for {
changed := false
checker.global_demands_dirty = false
checker.record_field_demands_dirty = false
spec_count := len(checker.specs)
for field_default in checker.ast_module.struct_field_defaults {
slot, field, ok := find_struct_field_slot(checker, field_default.record, field_default.field)
if !ok {
continue
}
expected := field.type if is_runtime_type(checker, field.type) else types.INVALID
inferred := infer_expr(
checker, field_default.expr, nil, field_default.pkg, field_default.file,
expected=expected,
)
_ = record_field_expr_candidate(
checker, slot, field_default.expr, inferred, nil, field_default.pkg, field_default.file,
)
if is_runtime_type(checker, checker.module.types.fields[slot].type) {
_ = record_demand(
checker, field_default.expr, checker.module.types.fields[slot].type,
nil, nil, field_default.pkg, field_default.file,
)
}
}
// 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 || global.diagnostic != source.INVALID_DIAGNOSTIC {
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 || global.diagnostic != source.INVALID_DIAGNOSTIC {
continue
}
declared := resolve_inferred_array(
checker,
type_from_syntax(checker, global.type, global.pkg, global.file),
global.expr,
)
expected := declared if is_runtime_type(checker, declared) || declared == types.UINT else types.INVALID
inferred := infer_expr(checker, global.expr, nil, global.pkg, global.file, expected=expected)
if resolved := resolve_inferred_array_from_type(checker, declared, inferred);
resolved != declared {
if !types.equal(checker.global_types[index], resolved) {
checker.global_types[index] = resolved
changed = true
}
continue
}
if is_runtime_type(checker, declared) && !has_inferred_array_count(checker, declared) {
continue
}
if types.is_constraint(declared) {
resolved := types.INVALID
if is_runtime_type(checker, checker.global_demands[index]) &&
types.constraint_accepts(declared, checker.global_demands[index], &checker.module.types) {
resolved = checker.global_demands[index]
} else {
resolved = types.constraint_target(declared, inferred, &checker.module.types)
}
if is_runtime_type(checker, resolved) && !types.equal(checker.global_types[index], resolved) {
checker.global_types[index] = resolved
changed = true
}
continue
}
if is_runtime_type(checker, checker.global_demands[index]) {
// A backward demand is authoritative; assign directly (it may cross the
// signed/unsigned family that widening would reject).
if !types.equal(checker.global_types[index], checker.global_demands[index]) {
checker.global_types[index] = checker.global_demands[index]
changed = true
}
} else if checker.global_open_const[index] || checker.global_open_float[index] {
// Defer defaulting: an undemanded open constant stays typeless during the
// fixpoint so its provisional smallest-signed default never leaks as a
// demand and poisons a sibling open constant used in the same arithmetic
// (e.g. the lagging type in `x += speed`). The pass after this loop assigns
// the default once the fixpoint settles and no further demand can arrive.
} else {
changed = merge_inferred_type(&checker.module.types, &checker.global_types[index], inferred) || changed
}
}
for index := 0; index < len(checker.specs); index += 1 {
id := spec_id(index)
before := checker.specs[id].result
inferred := infer_spec_result(checker, id)
changed = !types.equal(before, checker.specs[id].result) || changed
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 checker.record_field_demands_dirty {
changed = true
}
if !changed {
if !defaults_applied {
defaults_applied = true
defaulted := false
// No authoritative demand can still arrive. Assign final defaults, then
// continue the same fixpoint so dependent globals/specs observe them.
for global, index in checker.ast_module.globals {
if global.external || global.diagnostic != source.INVALID_DIAGNOSTIC ||
is_runtime_type(checker, checker.global_types[index]) {
continue
}
if checker.global_open_const[index] {
declared := type_from_syntax(checker, global.type, global.pkg, global.file)
checker.global_types[index] = constraint_integer_literal_type(declared, checker.global_const_value[index])
defaulted = true
} else if checker.global_open_float[index] {
checker.global_types[index] = types.F64
defaulted = true
}
}
for fallback, slot in checker.record_field_defaults {
if !is_inferred_record_field(checker, slot) ||
!types.is_constraint(checker.module.types.fields[slot].type) ||
!is_runtime_type(checker, fallback) {
continue
}
checker.module.types.fields[slot].type = fallback
defaulted = true
}
if defaulted {
continue
}
}
break
}
}
}
prune_specs :: proc(checker: ^Checker) {
stack: [dynamic]Spec_Id
stack.allocator = checker.allocator
defer delete(stack)
main_template := find_template(checker, checker.main_symbol, 0)
if main_template != ast.INVALID_FUNCTION {
mark_spec_demanded(checker, find_spec(checker, main_template, nil), &stack)
}
if checker.entry_point == .Process {
mark_spec_demanded(checker, find_spec(checker, checker.io_provider_template, nil), &stack)
}
for global, index in checker.ast_module.globals {
if global.external || global.diagnostic != source.INVALID_DIAGNOSTIC {
continue
}
expected := checker.global_types[index] if is_runtime_type(checker, checker.global_types[index]) else types.INVALID
_ = infer_expr(checker, global.expr, nil, global.pkg, global.file, &stack, expected=expected)
}
for len(stack) > 0 {
id := pop(&stack)
_ = infer_spec_result(checker, id, &stack)
}
retained := 0
for spec in checker.specs {
if spec.hir_id == hir.INVALID_FUNCTION {
delete(spec.args, checker.allocator)
delete(spec.comptime_values, checker.allocator)
continue
}
checker.specs[retained] = spec
checker.specs[retained].hir_id = hir.function_id(retained)
retained += 1
}
for len(checker.specs) > retained {
_ = pop(&checker.specs)
}
}
add_hir_expr :: proc(checker: ^Checker, expr: hir.Expr) -> hir.Expr_Id {
id := hir.expr_id(len(checker.module.exprs))
append(&checker.module.exprs, expr)
return id
}
invalid_hir_expr :: proc(
checker: ^Checker,
span: source.Span,
diagnostic: source.Diagnostic_Id,
recovery_type := types.INVALID,
) -> hir.Expr_Id {
return add_hir_expr(
checker,
hir.Expr {
kind = .Invalid,
span = span,
type = recovery_type,
target = hir.INVALID_REF,
left = hir.INVALID_EXPR,
right = hir.INVALID_EXPR,
diagnostic = diagnostic,
},
)
}
invalid_expr_diagnostic :: proc(checker: ^Checker, id: hir.Expr_Id) -> (source.Diagnostic_Id, bool) {
if id == hir.INVALID_EXPR || int(id) >= len(checker.module.exprs) {
return source.INVALID_DIAGNOSTIC, false
}
expr := checker.module.exprs[id]
return expr.diagnostic, expr.kind == .Invalid && expr.diagnostic != source.INVALID_DIAGNOSTIC
}
propagate_invalid_expr :: proc(checker: ^Checker, span: source.Span, values: ..hir.Expr_Id) -> (hir.Expr_Id, bool) {
for value in values {
if diagnostic, invalid := invalid_expr_diagnostic(checker, value); invalid {
return invalid_hir_expr(checker, span, diagnostic), true
}
}
return hir.INVALID_EXPR, false
}
add_unique_global :: proc(values: ^[dynamic]hir.Global_Id, value: hir.Global_Id) {
for existing in values {
if existing == value {
return
}
}
append(values, value)
}
build_global_reference :: proc(
checker: ^Checker,
global: ast.Global_Id,
span: source.Span,
global_reads: ^[dynamic]hir.Global_Id,
) -> hir.Expr_Id {
if int(global) < len(checker.external_global_diagnostics) {
diagnostic := checker.external_global_diagnostics[global]
if diagnostic != source.INVALID_DIAGNOSTIC {
return invalid_hir_expr(checker, span, diagnostic, checker.global_types[global])
}
}
hir_global := hir.Global_Id(global)
add_unique_global(global_reads, hir_global)
return add_hir_expr(checker, hir.Expr{
kind=.Global, span=span, type=checker.global_types[global],
target=hir.global_ref(hir_global), left=hir.INVALID_EXPR, right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
add_unique_function :: proc(values: ^[dynamic]hir.Function_Id, value: hir.Function_Id) {
for existing in values {
if existing == value {
return
}
}
append(values, value)
}
find_build_local :: proc(locals: []Build_Local, name: symbol.Id) -> (Build_Local, bool) {
for index := len(locals) - 1; index >= 0; index -= 1 {
if locals[index].name == name {
return locals[index], true
}
}
return Build_Local{}, false
}
can_implicitly_convert_type :: proc(checker: ^Checker, actual, expected: types.Type) -> bool {
store := &checker.module.types
if types.is_noreturn(actual) ||
types.equal(actual, expected) ||
types.can_widen(actual, expected) ||
types.can_coerce_c_integer(actual, expected, checker.target) ||
types.can_coerce_c_scalar(actual, expected, checker.target) ||
types.can_weaken_pointer(actual, expected, store) ||
types.can_coerce_function_pointer(actual, expected, store) ||
types.can_weaken_slice(actual, expected, store) ||
types.can_decay_slice_c_string(actual, expected, store) ||
types.can_decay_array_pointer(actual, expected, store) ||
types.can_sum_widen(actual, expected, store) {
return true
}
if types.is_optional(expected, store) {
return can_implicitly_convert_type(checker, actual, types.child_type(expected, store))
}
return false
}
coerce_expr :: proc(
checker: ^Checker,
expr_id: hir.Expr_Id,
expected: types.Type,
span: source.Span,
) -> hir.Expr_Id {
if expr_id == hir.INVALID_EXPR {
return expr_id
}
if _, invalid := invalid_expr_diagnostic(checker, expr_id); invalid {
return expr_id
}
actual := checker.module.exprs[expr_id].type
if types.is_noreturn(actual) {
return expr_id
}
if types.equal(actual, expected) {
return expr_id
}
if types.can_coerce_function_pointer(actual, expected, &checker.module.types) {
expr := checker.module.exprs[expr_id]
if expr.kind == .Function {
expr.type = expected
return add_hir_expr(checker, expr)
}
}
if types.can_weaken_pointer(actual, expected, &checker.module.types) {
return add_hir_expr(checker, hir.Expr{
kind=.Weaken_Pointer,
span=span,
type=expected,
left=expr_id,
target=hir.INVALID_REF,
right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
if types.can_weaken_slice(actual, expected, &checker.module.types) {
return add_hir_expr(checker, hir.Expr{
kind=.Weaken_Slice,
span=span,
type=expected,
left=expr_id,
target=hir.INVALID_REF,
right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
if types.can_decay_array_pointer(actual, expected, &checker.module.types) {
return add_hir_expr(checker, hir.Expr{
kind=.Decay_Array_Pointer,
span=span,
type=expected,
left=expr_id,
target=hir.INVALID_REF,
right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
if types.can_decay_slice_c_string(actual, expected, &checker.module.types) {
return add_hir_expr(checker, hir.Expr{
kind=.Slice_Ptr,
span=span,
type=expected,
left=expr_id,
target=hir.INVALID_REF,
right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
if types.can_sum_widen(actual, expected, &checker.module.types) {
return add_hir_expr(checker, hir.Expr{
kind=.Sum_Widen,
span=span,
type=expected,
left=expr_id,
target=hir.INVALID_REF,
right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
if types.is_optional(expected, &checker.module.types) {
child := types.child_type(expected, &checker.module.types)
if types.equal(actual, child) ||
types.can_coerce_function_pointer(actual, child, &checker.module.types) ||
types.can_widen(actual, child) ||
types.can_coerce_c_integer(actual, child, checker.target) ||
types.can_coerce_c_scalar(actual, child, checker.target) ||
types.can_weaken_pointer(actual, child, &checker.module.types) ||
types.can_weaken_slice(actual, child, &checker.module.types) ||
types.can_decay_slice_c_string(actual, child, &checker.module.types) ||
types.can_decay_array_pointer(actual, child, &checker.module.types) {
value := coerce_expr(checker, expr_id, child, span)
return add_hir_expr(checker, hir.Expr{
kind=.Optional_Some,
span=span,
type=expected,
left=value,
target=hir.INVALID_REF,
right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
}
if types.can_widen(actual, expected) {
return add_hir_expr(
checker,
hir.Expr {
kind = .Widen,
span = span,
type = expected,
left = expr_id,
target = hir.INVALID_REF,
right = hir.INVALID_EXPR,
diagnostic = source.INVALID_DIAGNOSTIC,
},
)
}
if types.can_coerce_c_integer(actual, expected, checker.target) ||
types.can_coerce_c_scalar(actual, expected, checker.target) {
return add_hir_expr(
checker,
hir.Expr {
kind = .C_Coerce,
span = span,
type = expected,
left = expr_id,
target = hir.INVALID_REF,
right = hir.INVALID_EXPR,
diagnostic = source.INVALID_DIAGNOSTIC,
},
)
}
id := source.addf(
checker.diagnostics,
span,
"cannot implicitly convert %s to %s",
type_label(checker, actual),
type_label(checker, expected),
)
return invalid_hir_expr(checker, span, id, expected)
}
promote_c_vararg_expr :: proc(checker: ^Checker, expr_id: hir.Expr_Id, span: source.Span) -> hir.Expr_Id {
actual := checker.module.exprs[expr_id].type
if !types.is_c_vararg_type(actual, &checker.module.types) {
id := source.addf(
checker.diagnostics,
span,
"C variadic argument must be a concrete scalar or pointer, got %s",
types.name(actual),
)
return invalid_hir_expr(checker, span, id, types.C_INT)
}
promoted := types.c_vararg_promotion(actual, checker.target, &checker.module.types)
if types.equal(actual, promoted) {
return expr_id
}
return add_hir_expr(checker, hir.Expr{
kind=.C_Vararg_Promote,
span=span,
type=promoted,
left=expr_id,
target=hir.INVALID_REF,
right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
build_constant_expr :: proc(
checker: ^Checker,
expr: ast.Expr,
constant: Constant,
expected: types.Type,
numeric_operation := false,
) -> hir.Expr_Id {
materialized := expected
nominal := types.INVALID
if numeric_operation {
if representation, ok := types.distinct_scalar_backing(expected, &checker.module.types);
ok && !types.is_bool(representation) {
materialized = representation
nominal = expected
}
}
recovery_type := types.I64
if types.is_concrete_integer(materialized) {
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(materialized, checker.target) {
fval := f64(constant.value) // ponytail: silent precision loss past 2^53, like C int->double
bits := transmute(i64)fval
if types.bits(materialized, checker.target) == 32 {
bits = i64(transmute(u32)f32(fval))
}
return add_hir_expr(checker, hir.Expr{
kind = .Float,
span = expr.span,
type = nominal if types.is_valid(nominal) else materialized,
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 == .Non_Exact {
id := source.add(checker.diagnostics, expr.span, "exact division has a remainder")
return invalid_hir_expr(checker, expr.span, id, recovery_type)
}
if constant.kind == .Integer_Division {
id := source.add(checker.diagnostics, expr.span, "integer '/' is not allowed; use divtrunc!, divfloor!, divexact!, or divceil!")
return invalid_hir_expr(checker, expr.span, id, recovery_type)
}
if constant.kind == .Overflow ||
(!types.is_concrete_integer(materialized) && materialized != types.UINT && !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 := constraint_integer_literal_type(materialized, constant.value)
if materialized == types.UINT && !types.is_valid(result_type) {
id := source.addf(
checker.diagnostics,
expr.span,
"integer constant %d does not satisfy uint",
constant.value,
)
return invalid_hir_expr(checker, expr.span, id, types.U64)
}
if types.is_concrete_integer(materialized) {
if !fits_integer_type(constant.value, materialized, 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 = nominal if types.is_valid(nominal) else materialized
}
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,
numeric_operation := false,
) -> hir.Expr_Id {
materialized := expected
result_type := types.F64
if numeric_operation {
if representation, ok := types.distinct_scalar_backing(expected, &checker.module.types);
ok && types.is_float(representation, checker.target) {
materialized = representation
result_type = expected
}
}
if types.is_float(materialized, checker.target) {
if !types.is_valid(result_type) || !types.is_distinct(result_type, &checker.module.types) {
result_type = materialized
}
} else if types.is_valid(materialized) {
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(materialized, checker.target) == 32 {
bits = i64(transmute(u32)f32(value))
}
return add_hir_expr(checker, hir.Expr{
kind=.Float,
span=expr.span,
type=result_type,
integer=bits,
target=hir.INVALID_REF,
left=hir.INVALID_EXPR,
right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
Build_Expr_Frame :: struct {
expr: ast.Expr_Id,
expected: types.Type,
target_type: types.Type,
stage: u8,
left: hir.Expr_Id,
arg_index: int,
arg_mode: Call_Argument_Mode,
prefix: int,
mapping: []int,
built_args: []hir.Expr_Id,
arg_types: []types.Type,
template: ast.Function_Id,
resolution: int,
numeric_operation: bool,
reverse_operands: bool,
}
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_struct_field_default :: proc(
checker: ^Checker,
struct_type: types.Type,
name: symbol.Id,
) -> (Resolved_Field_Default, []Comptime_Value, bool) {
resolved := types.resolve_alias(struct_type, &checker.module.types)
for field_default in checker.ast_module.struct_field_defaults {
if types.resolve_alias(field_default.record, &checker.module.types) == resolved &&
field_default.field == name {
return Resolved_Field_Default{
expr=field_default.expr,
pkg=field_default.pkg,
file=field_default.file,
static_value=INVALID_CT_VALUE,
span=checker.ast_module.exprs[field_default.expr].span,
}, nil, true
}
}
for entry in checker.generated_types {
if !types.equal(entry.result, resolved) || entry.expr == ast.INVALID_EXPR ||
int(entry.expr) >= len(checker.ast_module.exprs) {
continue
}
expr := checker.ast_module.exprs[entry.expr]
fields := types.fields_for(&checker.module.types, resolved)
for field, index in fields {
if field.name != u32(name) {
continue
}
if index < len(entry.defaults) && entry.defaults[index] != INVALID_CT_VALUE {
return Resolved_Field_Default{
expr=ast.INVALID_EXPR,
pkg=entry.pkg,
file=entry.file,
static_value=entry.defaults[index],
span=expr.span,
}, entry.values, true
}
if is_intrinsic_call(checker, expr, "struct_type") {
continue
}
if index < len(expr.args) && expr.args[index] != ast.INVALID_EXPR {
return Resolved_Field_Default{
expr=expr.args[index],
pkg=entry.pkg,
file=entry.file,
static_value=INVALID_CT_VALUE,
span=checker.ast_module.exprs[expr.args[index]].span,
}, entry.values, true
}
}
}
return {}, nil, false
}
find_tuple_field :: proc(checker: ^Checker, tuple_type: types.Type, index: u64) -> (int, types.Field, bool) {
item, ok := types.node(&checker.module.types, tuple_type)
if !ok || item.kind != .Struct || !item.tuple || index >= u64(item.field_count) {
return 0, {}, false
}
fields := types.fields_for(&checker.module.types, tuple_type)
return int(index), fields[index], true
}
find_struct_field_slot :: proc(checker: ^Checker, struct_type: types.Type, name: symbol.Id) -> (int, types.Field, bool) {
item, ok := types.node(&checker.module.types, struct_type)
if !ok || (item.kind != .Struct && item.kind != .Union) {
return -1, {}, false
}
for field, index in types.fields_for(&checker.module.types, struct_type) {
if field.name == u32(name) {
return int(item.field_start)+index, field, true
}
}
return -1, {}, false
}
field_type_from_value :: proc(checker: ^Checker, expr: ast.Expr, base_type: types.Type) -> types.Type {
store := &checker.module.types
if !symbol.is_valid(expr.name) {
value_type := types.child_type(base_type, store) if types.is_pointer(base_type, store) else base_type
_, field, ok := find_tuple_field(checker, value_type, expr.integer)
return field.type if ok else types.INVALID
}
field_name := symbol_text(checker, expr.name)
item, has_item := types.container(base_type, store)
if has_item && (item.kind == .Array || item.kind == .Slice) {
if field_name == "len" {
return types.USIZE
}
if field_name == "ptr" &&
(item.kind == .Slice || types.is_pointer(base_type, store)) {
return container_pointer_type(store, item)
}
}
value_type := base_type
if types.is_pointer(value_type, store) {
value_type = types.child_type(value_type, store)
}
_, field, ok := find_struct_field(checker, value_type, expr.name)
return field.type if ok else types.INVALID
}
infer_qualified_value_field_type :: proc(
checker: ^Checker,
expr: ast.Expr,
locals: []Infer_Local,
pkg: ast.Package_Id,
file: ast.File_Id,
demanded: ^[dynamic]Spec_Id = nil,
) -> (types.Type, bool) {
if !symbol.is_valid(expr.qualifier) ||
find_import(checker, file, expr.qualifier) != ast.INVALID_IMPORT {
return types.INVALID, false
}
base_type := find_infer_local(locals, expr.qualifier)
if !types.is_valid(base_type) {
if value, ok := current_comptime_value(checker, expr.qualifier); ok && value.kind == .Static {
base_type = value.type
if field_value, found := persistent_field_value(checker, value.static_value, expr.name);
found && field_value.kind == .Function {
_, _, _ = function_pointer_type_for_template(
checker, ast.Function_Id(u32(field_value.index)), demanded,
)
}
}
}
if !types.is_valid(base_type) {
if global := find_global(checker, expr.qualifier, pkg, file); global != ast.INVALID_GLOBAL {
base_type = checker.global_types[global]
}
}
if !types.is_valid(base_type) {
return types.INVALID, false
}
return field_type_from_value(checker, expr, base_type), true
}
build_field_from_value :: proc(
checker: ^Checker,
expr: ast.Expr,
base: hir.Expr_Id,
base_type: types.Type,
) -> (hir.Expr_Id, bool) {
store := &checker.module.types
if !symbol.is_valid(expr.name) {
value_type := types.child_type(base_type, store) if types.is_pointer(base_type, store) else base_type
index, field, ok := find_tuple_field(checker, value_type, expr.integer)
if !ok {
id := source.addf(checker.diagnostics, expr.span, "tuple field index %d is out of range or the value is not a tuple", expr.integer)
return invalid_hir_expr(checker, expr.span, id), false
}
return add_hir_expr(checker, hir.Expr{
kind=.Field, span=expr.span, type=field.type, integer=i64(index), left=base,
target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC,
}), true
}
field_name := symbol_text(checker, expr.name)
item, has_item := types.container(base_type, store)
if has_item && (item.kind == .Array || item.kind == .Slice) {
if field_name == "len" {
return add_hir_expr(checker, hir.Expr{
kind=.Length, span=expr.span, type=types.USIZE, left=base,
target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC,
}), true
}
if field_name == "ptr" &&
(item.kind == .Slice || types.is_pointer(base_type, store)) {
return add_hir_expr(checker, hir.Expr{
kind=.Slice_Ptr, span=expr.span,
type=container_pointer_type(store, item), left=base,
target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC,
}), true
}
if field_name == "ptr" && item.kind == .Array {
id := source.add(checker.diagnostics, expr.span, "arrays do not expose '.ptr'; take their address first")
return invalid_hir_expr(checker, expr.span, id), false
}
}
value_type := base_type
if types.is_pointer(value_type, store) {
value_type = types.child_type(value_type, store)
}
index, field, ok := find_struct_field(checker, value_type, expr.name)
if !ok {
id := source.addf(checker.diagnostics, expr.span, "unknown struct field '%s'", symbol_text(checker, expr.name))
return invalid_hir_expr(checker, expr.span, id), false
}
if types.is_void(field.type) {
id := source.addf(checker.diagnostics, expr.span, "variant '%s' has no payload to read", symbol_text(checker, expr.name))
return invalid_hir_expr(checker, expr.span, id), false
}
return add_hir_expr(checker, hir.Expr{
kind=.Field, span=expr.span, type=field.type, integer=i64(index), left=base,
target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC,
}), true
}
build_qualified_value_field :: proc(
checker: ^Checker,
expr: ast.Expr,
locals: []Build_Local,
global_reads: ^[dynamic]hir.Global_Id,
pkg: ast.Package_Id,
file: ast.File_Id,
) -> (hir.Expr_Id, bool, bool) {
if !symbol.is_valid(expr.qualifier) ||
find_import(checker, file, expr.qualifier, true) != ast.INVALID_IMPORT {
return hir.INVALID_EXPR, false, false
}
if local, ok := find_build_local(locals, expr.qualifier); ok {
base := build_local_expr(checker, local, expr.span)
value, ok := build_field_from_value(checker, expr, base, local.type)
return value, true, ok
}
if value, ok := current_comptime_value(checker, expr.qualifier);
ok && value.kind == .Static && value.static_value != INVALID_CT_VALUE &&
int(value.static_value) < len(checker.static_state.values) {
field_value, field_ok := persistent_field_value(checker, value.static_value, expr.name)
if !field_ok {
return hir.INVALID_EXPR, true, false
}
return build_static_value(checker, field_value, expr.span, types.INVALID), true, true
}
if global := find_global(checker, expr.qualifier, pkg, file); global != ast.INVALID_GLOBAL {
base := build_global_reference(checker, global, expr.span, global_reads)
value, ok := build_field_from_value(checker, expr, base, checker.global_types[global])
return value, true, ok
}
return hir.INVALID_EXPR, false, false
}
find_enum_member :: proc(checker: ^Checker, enum_type: types.Type, name: symbol.Id) -> (types.Enum_Member, bool) {
for member in types.enum_members_for(&checker.module.types, enum_type) {
if member.name == u32(name) {
return member, true
}
}
return {}, false
}
enum_type_from_name_expr :: proc(
checker: ^Checker,
expr: ast.Expr,
pkg: ast.Package_Id,
file: ast.File_Id,
) -> (types.Type, bool) {
if !symbol.is_valid(expr.qualifier) ||
find_import(checker, file, expr.qualifier) != ast.INVALID_IMPORT {
return types.INVALID, false
}
enum_type := types.find_named(&checker.module.types, u32(pkg), u32(expr.qualifier), file=u32(file))
return enum_type, types.is_enum(enum_type, &checker.module.types)
}
enum_type_from_field_expr :: proc(
checker: ^Checker,
expr: ast.Expr,
pkg: ast.Package_Id,
file: ast.File_Id,
mark_used := false,
) -> (types.Type, bool) {
if expr.left == ast.INVALID_EXPR || int(expr.left) >= len(checker.ast_module.exprs) {
return types.INVALID, false
}
base := checker.ast_module.exprs[expr.left]
if base.kind != .Name || !symbol.is_valid(base.qualifier) {
return types.INVALID, false
}
target_pkg, available := expr_package(checker, base, pkg, file, mark_used)
if !available {
return types.INVALID, false
}
enum_type := types.find_named(&checker.module.types, u32(target_pkg), u32(base.name), file=u32(expr_lookup_file(base, file)))
return enum_type, types.is_enum(enum_type, &checker.module.types)
}
enum_member_hir :: proc(
checker: ^Checker,
enum_type: types.Type,
name: symbol.Id,
span: source.Span,
) -> hir.Expr_Id {
member, ok := find_enum_member(checker, enum_type, name)
if !ok {
id := source.addf(checker.diagnostics, span, "unknown enum member '%s'", symbol_text(checker, name))
return invalid_hir_expr(checker, span, id, enum_type)
}
value := i64(member.value) if member.value < 0 else transmute(i64)u64(member.value)
return add_hir_expr(checker, hir.Expr{
kind=.Integer,
span=span,
type=enum_type,
integer=value,
target=hir.INVALID_REF,
left=hir.INVALID_EXPR,
right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
build_scalar_cast :: proc(
checker: ^Checker,
value: hir.Expr_Id,
target: types.Type,
span: source.Span,
) -> hir.Expr_Id {
store := &checker.module.types
actual := checker.module.exprs[value].type
if types.can_retype_distinct(actual, target, store) {
return add_hir_expr(checker, hir.Expr{
kind=.Retype,
span=span,
type=target,
left=value,
target=hir.INVALID_REF,
right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
valid_target := types.is_concrete_scalar(target) && !types.is_bool(target)
actual_repr := types.runtime_representation(actual, store)
actual_item, actual_item_ok := types.node(store, actual)
explicit_enum := actual_item_ok && actual_item.kind == .Enum && actual_item.explicit_backing
_, distinct_scalar := types.distinct_scalar_backing(actual, store)
valid_actual := (types.is_concrete_scalar(actual) || explicit_enum || distinct_scalar) &&
types.is_concrete_scalar(actual_repr) && !types.is_bool(actual_repr)
if !valid_target || !valid_actual {
id := source.addf(
checker.diagnostics,
span,
"scalar cast requires numeric scalar types, got %s to %s",
types.name(actual),
types.name(target),
)
return invalid_hir_expr(checker, span, id, target)
}
return add_hir_expr(checker, hir.Expr{
kind=.Scalar_Cast,
span=span,
type=target,
left=value,
target=hir.INVALID_REF,
right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
normalize_index_expr :: proc(checker: ^Checker, value: hir.Expr_Id, span: source.Span) -> hir.Expr_Id {
store := &checker.module.types
current := value
current_type := checker.module.exprs[current].type
representation, distinct_ok := types.distinct_scalar_backing(current_type, store)
if distinct_ok && types.is_concrete_integer(representation) {
for {
backing, ok := types.distinct_backing(current_type, store)
if !ok {
break
}
current = add_hir_expr(checker, hir.Expr{
kind=.Retype,
span=span,
type=backing,
left=current,
target=hir.INVALID_REF,
right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
current_type = backing
}
}
current_expr := &checker.module.exprs[current]
if current_expr.kind == .Integer &&
fits_integer_type(i128(current_expr.integer), types.USIZE, checker.target) {
current_expr.type = types.USIZE
return current
}
return coerce_expr(checker, current, types.USIZE, span)
}
build_function_value :: proc(
checker: ^Checker,
template: ast.Function_Id,
span: source.Span,
expected: types.Type,
) -> hir.Expr_Id {
if template == ast.INVALID_FUNCTION || int(template) >= len(checker.ast_module.functions) {
return hir.INVALID_EXPR
}
function := checker.ast_module.functions[template]
if len(function.unsupported_reason) > 0 {
id := source.addf(
checker.diagnostics,
span,
"C declaration '%s' is unavailable: %s",
symbol_text(checker, function.name),
function.unsupported_reason,
)
return invalid_hir_expr(checker, span, id)
}
if checker.template_diagnostics[template] != source.INVALID_DIAGNOSTIC {
return invalid_hir_expr(checker, span, checker.template_diagnostics[template])
}
params, result, ok := function_value_signature(checker, template)
if !ok {
id := source.addf(
checker.diagnostics,
span,
"function '%s' cannot be used as a function value; expected a concrete non-comptime signature",
symbol_text(checker, function.name),
)
return invalid_hir_expr(checker, span, id)
}
defer delete(params, checker.allocator)
function_type := types.function(&checker.module.types, params, result, function.c_abi, function.variadic)
value_type := function_type
expected_pointer := expected
if types.is_optional(expected_pointer, &checker.module.types) {
expected_pointer = types.child_type(expected_pointer, &checker.module.types)
}
if _, _, expected_function, ok := types.function_pointer(expected_pointer, &checker.module.types); ok &&
types.equal(expected_function, function_type) {
value_type = expected_pointer
} else if types.equal(expected, function_type) {
value_type = expected
}
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, value_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=value_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,
numeric_operation := false,
) -> 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, numeric_operation)
delete(checker.build_stack)
checker.build_stack = outer
return result
}
try_build_comptime_division :: proc(
checker: ^Checker,
expr: ast.Expr,
kind: Division_Builtin,
expected: types.Type,
pkg: ast.Package_Id,
file: ast.File_Id,
) -> (hir.Expr_Id, bool) {
state := ct_state_make(checker, pkg, file, diagnose=false)
defer ct_state_destroy(&state)
value, flow, ok := ct_eval_division_call(&state, expr, kind, expected, 0)
if ok && flow.kind == .Normal && value != INVALID_CT_VALUE {
return ct_materialize_value(&state, value, expr.span, expected), true
}
message := ""
#partial switch state.error {
case .Div_By_Zero: message = "division builtin denominator is zero"
case .Overflow: message = "signed integer division overflow"
case .Non_Exact: message = "exact division has a remainder"
}
if len(message) == 0 {
return hir.INVALID_EXPR, false
}
id := source.add(checker.diagnostics, expr.span, message)
recovery := expected if types.is_concrete_scalar(expected) else types.I64
return invalid_hir_expr(checker, expr.span, id, recovery), true
}
build_division_builtin :: proc(
checker: ^Checker,
expr: ast.Expr,
kind: Division_Builtin,
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 {
if len(expr.args) != 2 {
id := source.addf(
checker.diagnostics, expr.span, "%s! expects 2 arguments, got %d",
symbol_text(checker, expr.name), len(expr.args),
)
return invalid_hir_expr(checker, expr.span, id)
}
if value, handled := try_build_comptime_division(checker, expr, kind, expected, pkg, file); handled {
return value
}
hint := expected if types.is_concrete_scalar(expected) && !types.is_bool(expected) else types.INVALID
left_const := is_numeric_constant_expr(checker, expr.args[0])
right_const := is_numeric_constant_expr(checker, expr.args[1])
left, right := hir.INVALID_EXPR, hir.INVALID_EXPR
if left_const && !right_const && !types.is_valid(hint) {
right = build_nested_expr(checker, expr.args[1], locals, global_reads, calls, types.INVALID, pkg, file)
right_type := checker.module.exprs[right].type
_, distinct_ok := types.distinct_scalar_backing(right_type, &checker.module.types)
left = build_nested_expr(checker, expr.args[0], locals, global_reads, calls, right_type, pkg, file, distinct_ok)
} else {
left = build_nested_expr(checker, expr.args[0], locals, global_reads, calls, hint, pkg, file)
right_hint := hint if types.is_valid(hint) else checker.module.exprs[left].type
_, distinct_ok := types.distinct_scalar_backing(right_hint, &checker.module.types)
right = build_nested_expr(
checker, expr.args[1], locals, global_reads, calls, right_hint, pkg, file,
distinct_ok && right_const,
)
}
operation, ok := numeric_operation_type(
checker,
checker.module.exprs[left].type,
checker.module.exprs[right].type,
)
if !ok || types.is_bool(operation.representation) {
id := source.add(checker.diagnostics, expr.span, "division builtins require compatible numeric operands")
return invalid_hir_expr(checker, expr.span, id)
}
left = coerce_expr(checker, left, operation.result, checker.module.exprs[left].span)
right = coerce_expr(checker, right, operation.result, checker.module.exprs[right].span)
result_kind := hir.Expr_Kind.Div_Trunc
#partial switch kind {
case .Floor: result_kind = .Div_Floor
case .Exact: result_kind = .Div_Exact
case .Ceil: result_kind = .Div_Ceil
case .Rem: result_kind = .Rem
case .Mod: result_kind = .Mod
case:
}
return add_hir_expr(checker, hir.Expr{
kind=result_kind, span=expr.span, type=operation.result, left=left, right=right,
target=hir.INVALID_REF, diagnostic=source.INVALID_DIAGNOSTIC,
})
}
build_memory_builtin :: proc(
checker: ^Checker,
expr: ast.Expr,
kind: Memory_Builtin,
locals: []Build_Local,
global_reads: ^[dynamic]hir.Global_Id,
calls: ^[dynamic]hir.Function_Id,
pkg: ast.Package_Id,
file: ast.File_Id,
) -> hir.Expr_Id {
name := symbol_text(checker, expr.name)
if len(expr.args) != 2 {
id := source.addf(checker.diagnostics, expr.span, "%s! expects 2 arguments, got %d", name, len(expr.args))
return invalid_hir_expr(checker, expr.span, id, types.VOID)
}
destination := build_nested_expr(checker, expr.args[0], locals, global_reads, calls, types.INVALID, pkg, file)
destination_type := checker.module.exprs[destination].type
destination_child, destination_mutable, destination_ok := memory_region_type(checker, destination_type)
if !destination_ok || !destination_mutable {
id := source.addf(
checker.diagnostics, checker.ast_module.exprs[expr.args[0]].span,
"%s! destination must be a mutable slice or mutable pointer-to-array", name,
)
return invalid_hir_expr(checker, expr.span, id, types.VOID)
}
right := hir.INVALID_EXPR
result_kind := hir.Expr_Kind.Mem_Copy
if kind == .Set {
if is_undefined_expr(checker, expr.args[1]) {
right = add_hir_expr(checker, hir.Expr{
kind=.Undefined, span=checker.ast_module.exprs[expr.args[1]].span, type=destination_child,
target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
} else {
right = build_nested_expr(checker, expr.args[1], locals, global_reads, calls, destination_child, pkg, file)
right = coerce_expr(checker, right, destination_child, checker.ast_module.exprs[expr.args[1]].span)
}
result_kind = .Mem_Set
} else {
source_expr := build_nested_expr(checker, expr.args[1], locals, global_reads, calls, types.INVALID, pkg, file)
source_type := checker.module.exprs[source_expr].type
source_child, _, source_ok := memory_region_type(checker, source_type)
if !source_ok {
id := source.add(
checker.diagnostics, checker.ast_module.exprs[expr.args[1]].span,
"memcopy! source must be a slice or pointer-to-array",
)
return invalid_hir_expr(checker, expr.span, id, types.VOID)
}
if !types.equal(
types.resolve_alias(destination_child, &checker.module.types),
types.resolve_alias(source_child, &checker.module.types),
) {
id := source.add(checker.diagnostics, expr.span, "memcopy! source and destination element types must match")
return invalid_hir_expr(checker, expr.span, id, types.VOID)
}
right = source_expr
}
return add_hir_expr(checker, hir.Expr{
kind=result_kind, span=expr.span, type=types.VOID, left=destination, right=right,
target=hir.INVALID_REF, diagnostic=source.INVALID_DIAGNOSTIC,
})
}
fallible_aggregate :: proc(
checker: ^Checker,
span: source.Span,
channel: types.Type,
value: hir.Expr_Id,
error_path: bool,
) -> hir.Expr_Id {
values := make([]hir.Expr_Id, 1, checker.allocator)
values[0] = value
return add_hir_expr(checker, hir.Expr{
kind=.Struct,
span=span,
type=channel,
integer=1 if error_path else 0,
args=values,
target=hir.INVALID_REF,
left=hir.INVALID_EXPR,
right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
refined_error_projection :: proc(
ctx: ^Build_Ctx,
expr_id: hir.Expr_Id,
target: types.Type,
span: source.Span,
) -> (hir.Expr_Id, bool) {
checker := ctx.checker
if expr_id == hir.INVALID_EXPR || int(expr_id) >= len(checker.module.exprs) {
return hir.INVALID_EXPR, false
}
expr := checker.module.exprs[expr_id]
if expr.kind != .Local {
return hir.INVALID_EXPR, false
}
local := hir.as_local(expr.target)
if local == hir.INVALID_LOCAL || int(local) >= len(ctx.hir_locals^) || ctx.hir_locals^[local].mutable {
return hir.INVALID_EXPR, false
}
candidates: []u32
for index := len(ctx.error_refinements^) - 1; index >= 0; index -= 1 {
refinement := ctx.error_refinements^[index]
if refinement.local == local {
candidates = refinement.variants
break
}
}
if len(candidates) == 0 {
return hir.INVALID_EXPR, false
}
selected: [dynamic]u32
selected.allocator = checker.allocator
defer delete(selected)
for candidate in candidates {
allowed := true
for refinement in ctx.error_refinements^ {
if refinement.local != local {
continue
}
found := false
for variant in refinement.variants {
found = found || variant == candidate
}
allowed = allowed && found
}
if allowed {
append(&selected, candidate)
}
}
if !types.selected_sum_fits(expr.type, target, selected[:], &checker.module.types) {
return hir.INVALID_EXPR, false
}
return add_hir_expr(checker, hir.Expr{
kind=.Sum_Project, span=span, type=target, left=expr_id,
target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC,
}), true
}
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 .Null:
if !types.is_optional(expected, store) {
id := source.add(checker.diagnostics, expr.span, "'null' requires an optional context")
return invalid_hir_expr(checker, expr.span, id, expected)
}
return add_hir_expr(checker, hir.Expr{
kind=.Null, span=expr.span, type=expected, target=hir.INVALID_REF,
left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC,
})
case .Unreachable:
return add_hir_expr(checker, hir.Expr{
kind=.Unreachable, span=expr.span, type=types.NORETURN, 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 declaration initializer",
)
return invalid_hir_expr(checker, expr.span, id, expected)
case .Enum_Literal:
literal_expected := expected
optional_expected := types.is_optional(expected, store)
if optional_expected {
literal_expected = types.child_type(expected, store)
}
if types.is_tagged_union(literal_expected, store) {
index, field, found := find_struct_field(checker, literal_expected, expr.name)
if !found {
id := source.addf(checker.diagnostics, expr.span, "unknown variant '.%s' on '%s'", symbol_text(checker, expr.name), type_label(checker, literal_expected))
return invalid_hir_expr(checker, expr.span, id, expected)
}
values := make([]hir.Expr_Id, 1, checker.allocator)
if expr.left == ast.INVALID_EXPR {
if !types.is_void(field.type) {
id := source.addf(checker.diagnostics, expr.span, "variant '.%s' on '%s' needs a payload; only void variants can be built from a bare '.%s'",
symbol_text(checker, expr.name), type_label(checker, literal_expected), symbol_text(checker, expr.name))
delete(values, checker.allocator)
return invalid_hir_expr(checker, expr.span, id, expected)
}
values[0] = hir.INVALID_EXPR
} else {
if types.is_void(field.type) {
id := source.addf(checker.diagnostics, expr.span, "void variant '%s' takes no value", symbol_text(checker, expr.name))
delete(values, checker.allocator)
return invalid_hir_expr(checker, expr.span, id, expected)
}
values[0] = build_nested_expr(checker, expr.left, locals, global_reads, calls, field.type, pkg, file)
values[0] = coerce_expr(checker, values[0], field.type, checker.module.exprs[values[0]].span)
}
result := add_hir_expr(checker, hir.Expr{
kind=.Struct, span=expr.span, type=literal_expected, args=values, integer=i64(index),
target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
return coerce_expr(checker, result, expected, expr.span) if optional_expected else result
}
if expr.left != ast.INVALID_EXPR {
id := source.addf(
checker.diagnostics,
expr.span,
"'.%s{...}' requires a tagged-union context",
symbol_text(checker, expr.name),
)
return invalid_hir_expr(checker, expr.span, id, expected)
}
if !types.is_enum(literal_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)
}
result := enum_member_hir(checker, literal_expected, expr.name, expr.span)
return coerce_expr(checker, result, expected, expr.span) if optional_expected else result
case .Cast:
target := type_from_syntax(checker, expr.type, pkg, file)
value := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file)
return build_scalar_cast(checker, value, target, expr.span)
case .Address:
// `&<array literal>` (Zig's `&.{...}`): the operand is an rvalue with no
// address, so promote it to an anonymous global constant and take *its*
// address. Reuses the existing non-scalar-global storage path; only the
// stable global address enters the expression, so it never dangles. The
// resulting `*[N]T` then decays to a slice via the usual coercion.
if expr.left != ast.INVALID_EXPR && checker.ast_module.exprs[expr.left].kind == .Array {
operand := checker.ast_module.exprs[expr.left]
// Propagate an element-expected type through `&` so literal elements
// coerce to the target slice's element type (e.g. string -> []u8).
// Without this, `&["x"]` infers `*[1]*[N:0]u8`, which won't decay to
// `[][]u8` because can_decay_array_pointer requires child equality.
element := types.INVALID
if node, ok := types.node(store, expected); ok && (node.kind == .Slice || node.kind == .Array) {
element = node.child
}
synth_expected := types.INVALID
if types.is_valid(element) {
synth_expected = types.array(store, element, u64(len(operand.args)), false)
}
value := build_nested_expr(checker, expr.left, locals, global_reads, calls, synth_expected, pkg, file)
array_type := checker.module.exprs[value].type
hidden_id := hir.Global_Id(len(checker.ast_module.globals) + len(checker.anon_globals))
append(&checker.anon_globals, hir.Global{
name = symbol.intern(checker.symbols, "__anon.array"),
type = array_type,
expr = value,
writable = false,
external = false,
diagnostic = source.INVALID_DIAGNOSTIC,
})
add_unique_global(global_reads, hidden_id)
global_ref := add_hir_expr(checker, hir.Expr{
kind=.Global, span=expr.span, type=array_type, target=hir.global_ref(hidden_id),
left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC,
})
return add_hir_expr(checker, hir.Expr{
kind=.Address, span=expr.span, type=types.pointer(store, array_type, false, false),
left=global_ref, target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC,
})
}
value := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file)
if invalid, propagated := propagate_invalid_expr(checker, expr.span, value); propagated {
return invalid
}
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)
if invalid, propagated := propagate_invalid_expr(checker, expr.span, pointer); propagated {
return invalid
}
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.INVALID, pkg, file)
index = normalize_index_expr(checker, index, expr.span)
if invalid, propagated := propagate_invalid_expr(checker, expr.span, container, index); propagated {
return invalid
}
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:
text, comptime_ok := comptime_slice_bytes(checker, expr, pkg, file)
if comptime_ok {
string_id := intern_comptime_string(checker, text)
return add_hir_expr(checker, hir.Expr{
kind=.String, span=expr.span, type=string_literal_type(checker, string_id), integer=i64(string_id),
target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
container := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file)
if invalid, propagated := propagate_invalid_expr(checker, expr.span, container); propagated {
return invalid
}
container_type := checker.module.exprs[container].type
item, ok := types.container(container_type, store)
if !ok {
id := source.add(checker.diagnostics, expr.span, "slicing requires an array, slice, pointer-to-array, or many-item pointer")
return invalid_hir_expr(checker, expr.span, id)
}
if item.kind == .Pointer && expr.args[1] == ast.INVALID_EXPR {
id := source.add(checker.diagnostics, expr.span, "many-item pointer slicing requires an explicit end bound")
return invalid_hir_expr(checker, expr.span, id)
}
bounds := make([]hir.Expr_Id, 2, checker.allocator)
bounds[0] = hir.INVALID_EXPR
bounds[1] = hir.INVALID_EXPR
for bound, index in expr.args {
if bound != ast.INVALID_EXPR {
bounds[index] = build_nested_expr(checker, bound, locals, global_reads, calls, types.INVALID, pkg, file)
bounds[index] = normalize_index_expr(checker, bounds[index], checker.ast_module.exprs[bound].span)
if invalid, propagated := propagate_invalid_expr(checker, expr.span, bounds[index]); propagated {
delete(bounds, checker.allocator)
return invalid
}
}
}
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)
if invalid, propagated := propagate_invalid_expr(checker, expr.span, base); propagated {
return invalid
}
base_type := checker.module.exprs[base].type
result, _ := build_field_from_value(checker, expr, base, base_type)
return result
case .Unwrap:
optional := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file)
if invalid, propagated := propagate_invalid_expr(checker, expr.span, optional); propagated {
return invalid
}
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)
if invalid, propagated := propagate_invalid_expr(checker, expr.span, optional); propagated {
return invalid
}
optional_type := checker.module.exprs[optional].type
if !types.is_optional(optional_type, store) {
id := source.add(checker.diagnostics, expr.span, "'orelse' requires an optional left operand")
return invalid_hir_expr(checker, expr.span, id)
}
child := types.child_type(optional_type, store)
fallback := build_nested_expr(checker, expr.right, locals, global_reads, calls, child, pkg, file)
fallback = coerce_expr(checker, fallback, child, checker.module.exprs[fallback].span)
return add_hir_expr(checker, hir.Expr{
kind=.Orelse, span=expr.span, type=child, left=optional, right=fallback,
target=hir.INVALID_REF, diagnostic=source.INVALID_DIAGNOSTIC,
})
case .Try:
if checker.current_build_ctx != nil && checker.current_build_ctx.defer_depth > 0 {
id := source.add(checker.diagnostics, expr.span, "cannot 'try' inside a 'defer'")
return invalid_hir_expr(checker, expr.span, id)
}
left_expected := expected if checker.ast_module.exprs[expr.left].kind == .Call else types.INVALID
channel := build_nested_expr(checker, expr.left, locals, global_reads, calls, left_expected, pkg, file)
if diagnostic, invalid := invalid_expr_diagnostic(checker, channel); invalid {
return invalid_hir_expr(checker, expr.span, diagnostic)
}
channel_type := checker.module.exprs[channel].type
success := types.fallible_success(channel_type, store)
if !types.is_valid(success) {
id := source.add(checker.diagnostics, expr.span, "'try' requires a fallible expression")
return invalid_hir_expr(checker, expr.span, id)
}
enclosing_success := types.fallible_success(checker.current_result, store)
enclosing_error := types.fallible_error(checker.current_result, store)
if !types.is_valid(enclosing_success) {
id := source.add(checker.diagnostics, expr.span, "'try' requires an enclosing fallible function")
return invalid_hir_expr(checker, expr.span, id, success)
}
error_type := types.fallible_error(channel_type, store)
if !types.equal(error_type, enclosing_error) &&
!types.can_sum_widen(error_type, enclosing_error, store) {
id := source.add(checker.diagnostics, expr.span, "'try' error channel cannot be widened to the enclosing error channel")
return invalid_hir_expr(checker, expr.span, id, success)
}
cleanup: []hir.Stmt_Id
captures: []hir.Expr_Id
if checker.current_build_ctx != nil {
cleanup, captures = try_cleanup(checker.current_build_ctx)
}
return add_hir_expr(checker, hir.Expr{
kind=.Try,
span=expr.span,
type=success,
left=channel,
body=cleanup,
args=captures,
target=hir.INVALID_REF,
right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
case .Catch:
left_expected := expected if checker.ast_module.exprs[expr.left].kind == .Call else types.INVALID
channel := build_nested_expr(checker, expr.left, locals, global_reads, calls, left_expected, pkg, file)
if diagnostic, invalid := invalid_expr_diagnostic(checker, channel); invalid {
return invalid_hir_expr(checker, expr.span, diagnostic)
}
channel_type := checker.module.exprs[channel].type
success := types.fallible_success(channel_type, store)
if !types.is_valid(success) {
id := source.add(checker.diagnostics, expr.span, "'catch' requires a fallible expression")
return invalid_hir_expr(checker, expr.span, id)
}
body: []hir.Stmt_Id
capture := hir.INVALID_LOCAL
block_handler := false
void_fallthrough := false
fallback := hir.INVALID_EXPR
ctx := checker.current_build_ctx
capture_start := 0
if symbol.is_valid(expr.name) && expr.name != checker.sink_symbol {
if ctx == nil {
id := source.add(checker.diagnostics, expr.span, "a captured catch fallback is only valid in a function body")
return invalid_hir_expr(checker, expr.span, id, success)
}
capture_start = len(ctx.locals^)
error_type := types.fallible_error(channel_type, store)
capture = append_build_local(ctx, expr.name, error_type, false, expr.span)
}
if expr.right != ast.INVALID_EXPR {
fallback_locals := locals
if capture != hir.INVALID_LOCAL {
fallback_locals = ctx.locals^[:]
}
fallback = build_nested_expr(checker, expr.right, fallback_locals, global_reads, calls, success, pkg, file)
fallback = coerce_expr(checker, fallback, success, checker.module.exprs[fallback].span)
} else {
block_handler = true
if ctx == nil {
id := source.add(checker.diagnostics, expr.span, "catch block form is only valid in a function body")
return invalid_hir_expr(checker, expr.span, id, success)
}
handler: [dynamic]hir.Stmt_Id
handler.allocator = checker.allocator
fallback, _ = build_value_source(ctx, &handler, expr.body, success, expr.span, value_control_flow=expr.integer != 0, allow_exit=true)
void_fallthrough = fallback == hir.INVALID_EXPR && !all_paths_exit(&checker.module, handler[:])
body = handler[:]
}
if capture != hir.INVALID_LOCAL {
resize(ctx.locals, capture_start)
}
catch_mode := hir.CATCH_EXPRESSION
if block_handler {
catch_mode = hir.CATCH_VOID_FALLTHROUGH if void_fallthrough else hir.CATCH_BLOCK
}
return add_hir_expr(checker, hir.Expr{
kind=.Catch,
span=expr.span,
type=success,
integer=catch_mode,
left=channel,
right=fallback,
body=body,
target=hir.local_ref(capture) if capture != hir.INVALID_LOCAL else hir.INVALID_REF,
diagnostic=source.INVALID_DIAGNOSTIC,
})
case .Range:
expected_child := types.INVALID
if types.is_range(expected, store) {
expected_child = types.child_type(expected, store)
}
left_const := eval_constant(checker, expr.left)
right_const := eval_constant(checker, expr.right)
left, right: hir.Expr_Id
if types.is_valid(expected_child) {
left = build_nested_expr(checker, expr.left, locals, global_reads, calls, expected_child, pkg, file)
right = build_nested_expr(checker, expr.right, locals, global_reads, calls, expected_child, pkg, file)
} else if right_const.kind == .Value && left_const.kind != .Value {
left = build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file)
right = build_nested_expr(checker, expr.right, locals, global_reads, calls, checker.module.exprs[left].type, pkg, file)
} else if left_const.kind == .Value && right_const.kind != .Value {
right = build_nested_expr(checker, expr.right, locals, global_reads, calls, types.INVALID, pkg, file)
left = build_nested_expr(checker, expr.left, locals, global_reads, calls, checker.module.exprs[right].type, pkg, file)
} else {
left = build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file)
right = build_nested_expr(checker, expr.right, locals, global_reads, calls, types.INVALID, pkg, file)
}
if invalid, propagated := propagate_invalid_expr(checker, expr.span, left, right); propagated {
return invalid
}
child := expected_child
if !types.is_valid(child) {
child = types.widest(checker.module.exprs[left].type, checker.module.exprs[right].type)
}
if !types.is_concrete_integer(child) {
id := source.add(checker.diagnostics, expr.span, "range bounds must be compatible concrete integers")
return invalid_hir_expr(checker, expr.span, id)
}
left = coerce_expr(checker, left, child, checker.module.exprs[left].span)
right = coerce_expr(checker, right, child, checker.module.exprs[right].span)
args := make([]hir.Expr_Id, 2, checker.allocator)
args[0] = left
args[1] = right
return add_hir_expr(checker, hir.Expr{
kind=.Range, span=expr.span, type=types.range(store, child),
integer=i64(expr.integer), args=args,
target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
case .Comptime:
return build_comptime_expr(checker, expr, expected, pkg, file)
case .Bool:
return add_hir_expr(checker, hir.Expr{
kind=.Bool, span=expr.span, type=types.BOOL, integer=i64(expr.integer),
target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
case .Not:
operand := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.BOOL, pkg, file)
if invalid, propagated := propagate_invalid_expr(checker, expr.span, operand); propagated {
return invalid
}
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 .Bit_Not:
operand := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file)
if invalid, propagated := propagate_invalid_expr(checker, expr.span, operand); propagated {
return invalid
}
operand_type := checker.module.exprs[operand].type
representation := types.runtime_representation(operand_type, store)
if !types.is_concrete_integer(representation) {
id := source.add(checker.diagnostics, expr.span, "'~' requires a concrete integer operand")
return invalid_hir_expr(checker, expr.span, id, operand_type)
}
return add_hir_expr(checker, hir.Expr{
kind=.Bit_Not, span=expr.span, type=operand_type, left=operand,
target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC,
})
case .Bit_And, .Bit_Or, .Bit_Xor:
left_const := is_numeric_constant_expr(checker, expr.left)
right_const := is_numeric_constant_expr(checker, expr.right)
left, right: hir.Expr_Id
if types.is_concrete_integer(expected) {
left = build_nested_expr(checker, expr.left, locals, global_reads, calls, expected, pkg, file)
right = build_nested_expr(checker, expr.right, locals, global_reads, calls, expected, pkg, file)
} else if left_const && !right_const {
right = build_nested_expr(checker, expr.right, locals, global_reads, calls, types.INVALID, pkg, file)
right_type := checker.module.exprs[right].type
_, distinct_ok := types.distinct_scalar_backing(right_type, store)
left = build_nested_expr(checker, expr.left, locals, global_reads, calls, right_type, pkg, file, distinct_ok)
} else {
left = build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file)
right_hint := checker.module.exprs[left].type
_, distinct_ok := types.distinct_scalar_backing(right_hint, store)
right = build_nested_expr(
checker, expr.right, locals, global_reads, calls, right_hint, pkg, file,
distinct_ok && right_const,
)
}
if invalid, propagated := propagate_invalid_expr(checker, expr.span, left, right); propagated {
return invalid
}
operation, ok := numeric_operation_type(
checker,
checker.module.exprs[left].type,
checker.module.exprs[right].type,
)
if !ok || !types.is_concrete_integer(operation.representation) {
id := source.add(checker.diagnostics, expr.span, "bitwise operation requires compatible concrete integer operands")
return invalid_hir_expr(checker, expr.span, id)
}
left = coerce_expr(checker, left, operation.result, checker.module.exprs[left].span)
right = coerce_expr(checker, right, operation.result, checker.module.exprs[right].span)
kind := hir.Expr_Kind.Bit_And
#partial switch expr.kind {
case .Bit_Or: kind = .Bit_Or
case .Bit_Xor: kind = .Bit_Xor
}
return add_hir_expr(checker, hir.Expr{
kind=kind, span=expr.span, type=operation.result, left=left, right=right,
target=hir.INVALID_REF, diagnostic=source.INVALID_DIAGNOSTIC,
})
case .Shift_Left, .Shift_Right, .Shift_Left_Saturating:
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.U64, pkg, file)
if invalid, propagated := propagate_invalid_expr(checker, expr.span, left, right); propagated {
return invalid
}
left_type := checker.module.exprs[left].type
right_type := checker.module.exprs[right].type
left_representation := types.runtime_representation(left_type, store)
if !types.is_concrete_integer(left_representation) {
id := source.add(checker.diagnostics, checker.module.exprs[left].span, "shifted value must be a concrete integer")
return invalid_hir_expr(checker, expr.span, id, left_type)
}
if !types.is_unsigned(right_type, checker.target) {
id := source.add(checker.diagnostics, checker.module.exprs[right].span, "shift count must be an unsigned integer")
return invalid_hir_expr(checker, expr.span, id, left_type)
}
if constant := eval_integer_constant_in_context(checker, expr.right, pkg, file);
constant.kind == .Value && expr.kind != .Shift_Left_Saturating &&
constant.value >= i128(types.bits(left_representation, checker.target)) {
id := source.addf(
checker.diagnostics, checker.module.exprs[right].span,
"shift count %d exceeds %s width", constant.value, types.name(left_type),
)
return invalid_hir_expr(checker, expr.span, id, left_type)
}
kind := hir.Expr_Kind.Shift_Left
#partial switch expr.kind {
case .Shift_Right: kind = .Shift_Right
case .Shift_Left_Saturating: kind = .Shift_Left_Saturating
}
return add_hir_expr(checker, hir.Expr{
kind=kind, span=expr.span, type=left_type, left=left, right=right,
target=hir.INVALID_REF, 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)
if invalid, propagated := propagate_invalid_expr(checker, expr.span, left, right); propagated {
return invalid
}
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 literals whose type comes from their peer. This covers
// integer and enum literals as well as optional presence tests such as
// `value == null` and `null != value`.
left_const := eval_constant(checker, expr.left)
right_const := eval_constant(checker, expr.right)
left, right: hir.Expr_Id
left_expr := checker.ast_module.exprs[expr.left]
right_expr := checker.ast_module.exprs[expr.right]
left_numeric_const := left_const.kind == .Value || is_float_constant_expr(checker, expr.left)
right_numeric_const := right_const.kind == .Value || is_float_constant_expr(checker, expr.right)
if right_expr.kind == .Null && left_expr.kind != .Null {
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 == .Null && right_expr.kind != .Null {
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_expr.kind == .Enum_Literal && left_expr.kind != .Enum_Literal {
left = build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file)
right = build_nested_expr(checker, expr.right, locals, global_reads, calls, checker.module.exprs[left].type, pkg, file)
} else if left_expr.kind == .Enum_Literal && right_expr.kind != .Enum_Literal {
right = build_nested_expr(checker, expr.right, locals, global_reads, calls, types.INVALID, pkg, file)
left = build_nested_expr(checker, expr.left, locals, global_reads, calls, checker.module.exprs[right].type, pkg, file)
} else if right_numeric_const && !left_numeric_const {
left = build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file)
hint := checker.module.exprs[left].type
_, distinct_ok := types.distinct_scalar_backing(hint, store)
right = build_nested_expr(checker, expr.right, locals, global_reads, calls, hint, pkg, file, distinct_ok)
} else if left_numeric_const && !right_numeric_const {
right = build_nested_expr(checker, expr.right, locals, global_reads, calls, types.INVALID, pkg, file)
hint := checker.module.exprs[right].type
_, distinct_ok := types.distinct_scalar_backing(hint, store)
left = build_nested_expr(checker, expr.left, locals, global_reads, calls, hint, pkg, file, distinct_ok)
} 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)
}
if invalid, propagated := propagate_invalid_expr(checker, expr.span, left, right); propagated {
return invalid
}
left_type := checker.module.exprs[left].type
right_type := checker.module.exprs[right].type
operand_type := types.INVALID
if left_expr.kind == .Null || right_expr.kind == .Null {
if expr.kind != .Eq && expr.kind != .Ne ||
!types.is_optional(left_type, store) || !types.equal(left_type, right_type) {
id := source.add(checker.diagnostics, expr.span, "'null' only supports '==' and '!=' with an optional value")
return invalid_hir_expr(checker, expr.span, id, types.BOOL)
}
operand_type = left_type
} else 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 {
operation, ok := numeric_operation_type(checker, left_type, right_type)
if !ok ||
types.is_bool(operation.representation) && expr.kind != .Eq && expr.kind != .Ne {
id := source.add(checker.diagnostics, expr.span, "comparison requires compatible numeric operands")
return invalid_hir_expr(checker, expr.span, id, types.BOOL)
}
operand_type = operation.result
}
left = coerce_expr(checker, left, operand_type, checker.module.exprs[left].span)
right = coerce_expr(checker, right, operand_type, checker.module.exprs[right].span)
compare_kind := hir.Expr_Kind.Eq
#partial switch expr.kind {
case .Eq: compare_kind = .Eq
case .Ne: compare_kind = .Ne
case .Lt: compare_kind = .Lt
case .Le: compare_kind = .Le
case .Gt: compare_kind = .Gt
case .Ge: compare_kind = .Ge
}
return add_hir_expr(checker, hir.Expr{
kind=compare_kind, span=expr.span, type=types.BOOL, left=left, right=right,
target=hir.INVALID_REF, diagnostic=source.INVALID_DIAGNOSTIC,
})
case .Struct_Literal:
struct_type := types.INVALID
if expr.left != ast.INVALID_EXPR {
struct_type, _ = resolve_type_argument(checker, expr.left, pkg, file)
struct_type = types.resolve_alias(struct_type, store)
} else if symbol.is_valid(expr.name) {
target_pkg, available := expr_package(checker, expr, pkg, file, true)
struct_type = types.find_named(store, u32(target_pkg), u32(expr.name), file=u32(expr_lookup_file(expr, file))) if available else types.INVALID
struct_type = types.resolve_alias(struct_type, store)
if !types.is_record(struct_type, store) || types.is_opaque_struct(struct_type, store) {
id := source.addf(checker.diagnostics, expr.span, "unknown or opaque record type '%s'", symbol_text(checker, expr.name))
return invalid_hir_expr(checker, expr.span, id)
}
} else if expr.tuple {
struct_type = types.resolve_alias(expected, store)
} else {
struct_type = types.resolve_alias(expected, store)
if types.is_valid(struct_type) &&
(!types.is_struct(struct_type, store) || types.is_opaque_struct(struct_type, store)) {
id := source.add(checker.diagnostics, expr.span, "keyed contextual payload requires a struct payload")
return invalid_hir_expr(checker, expr.span, id, expected)
}
}
tuple_literal := expr.tuple
if tuple_literal && len(expr.args) == 0 && types.is_valid(struct_type) {
item, ok := types.node(store, struct_type)
tuple_literal = !ok || item.kind != .Struct || item.tuple
}
if tuple_literal {
resolved := types.is_valid(struct_type)
if resolved {
item, ok := types.node(store, struct_type)
if !ok || item.kind != .Struct || !item.tuple {
id := source.add(checker.diagnostics, expr.span, "positional construction requires a tuple type")
return invalid_hir_expr(checker, expr.span, id, struct_type)
}
}
fields := types.fields_for(store, struct_type) if resolved else nil
if resolved && len(expr.args) != len(fields) {
id := source.addf(checker.diagnostics, expr.span, "tuple literal expects %d elements, got %d", len(fields), len(expr.args))
return invalid_hir_expr(checker, expr.span, id, struct_type)
}
values := make([]hir.Expr_Id, len(expr.args), checker.allocator)
inferred_fields := make([]types.Field, len(expr.args), checker.allocator)
defer delete(inferred_fields, checker.allocator)
for arg, index in expr.args {
expected_field := fields[index].type if resolved else types.INVALID
values[index] = build_nested_expr(checker, arg, locals, global_reads, calls, expected_field, pkg, file)
if resolved {
values[index] = coerce_expr(checker, values[index], expected_field, checker.ast_module.exprs[arg].span)
} else {
inferred_fields[index].type = checker.module.exprs[values[index]].type
}
}
if !resolved {
struct_type = types.struct_anonymous(store, inferred_fields, true)
}
return add_hir_expr(checker, hir.Expr{
kind=.Struct, span=expr.span, type=struct_type, args=values,
target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
if !types.is_valid(struct_type) {
fields := make([]types.Field, len(expr.args), checker.allocator)
defer delete(fields, checker.allocator)
values := make([]hir.Expr_Id, len(expr.args), checker.allocator)
for keyed, index in expr.args {
keyed_expr := checker.ast_module.exprs[keyed]
fields[index].name = u32(keyed_expr.name)
for previous in fields[:index] {
if previous.name == fields[index].name {
source.addf(
checker.diagnostics, keyed_expr.span,
"duplicate initializer for struct field '%s'",
symbol_text(checker, keyed_expr.name),
)
}
}
values[index] = build_nested_expr(
checker, keyed_expr.left, locals, global_reads, calls, types.INVALID, pkg, file,
)
fields[index].type = checker.module.exprs[values[index]].type
}
struct_type = types.struct_anonymous(store, fields)
return add_hir_expr(checker, hir.Expr{
kind=.Struct, span=expr.span, type=struct_type, args=values,
target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
fields := types.fields_for(store, struct_type)
union_record := types.is_union(struct_type, store)
if union_record && len(expr.args) != 1 {
id := source.add(checker.diagnostics, expr.span, "union literal requires exactly one field initializer")
return invalid_hir_expr(checker, expr.span, id, struct_type)
}
values := make([]hir.Expr_Id, 1 if union_record else len(fields), checker.allocator)
initialized := make([]bool, len(fields), checker.allocator)
defer delete(initialized, checker.allocator)
for &value in values {
value = hir.INVALID_EXPR
}
for keyed in expr.args {
keyed_expr := checker.ast_module.exprs[keyed]
index, field, ok := find_struct_field(checker, struct_type, keyed_expr.name)
if !ok {
source.addf(checker.diagnostics, keyed_expr.span, "unknown struct field '%s'", symbol_text(checker, keyed_expr.name))
continue
}
if initialized[index] {
source.addf(checker.diagnostics, keyed_expr.span, "duplicate initializer for struct field '%s'", symbol_text(checker, keyed_expr.name))
continue
}
initialized[index] = true
value_index := 0 if union_record else index
is_void_field := union_record && types.is_void(field.type)
if keyed_expr.left == ast.INVALID_EXPR {
// Bare key `T{ variant }`: valid only to construct a void-payload
// variant (no value); the payload slot stays INVALID_EXPR.
if !is_void_field {
source.addf(checker.diagnostics, keyed_expr.span, "field '%s' requires a value", symbol_text(checker, keyed_expr.name))
}
} else {
if is_void_field {
source.addf(checker.diagnostics, keyed_expr.span, "void variant '%s' takes no value", symbol_text(checker, keyed_expr.name))
}
values[value_index] = build_nested_expr(checker, keyed_expr.left, locals, global_reads, calls, field.type, pkg, file)
values[value_index] = coerce_expr(checker, values[value_index], field.type, keyed_expr.span)
}
}
if !union_record {
for field, index in fields {
if values[index] != hir.INVALID_EXPR {
continue
}
if field_default, default_values, ok := find_struct_field_default(checker, struct_type, symbol.Id(field.name)); ok {
if field_default.static_value != INVALID_CT_VALUE {
if int(field_default.static_value) >= len(checker.static_state.values) {
continue
}
values[index] = build_static_value(
checker, checker.static_state.values[field_default.static_value], field_default.span, field.type,
)
values[index] = coerce_expr(checker, values[index], field.type, field_default.span)
continue
}
previous := checker.current_comptime_values
checker.current_comptime_values = default_values
values[index] = build_nested_expr(
checker, field_default.expr, nil, global_reads, calls,
field.type, field_default.pkg, field_default.file,
)
checker.current_comptime_values = previous
values[index] = coerce_expr(
checker, values[index], field.type,
field_default.span,
)
continue
}
id := source.addf(checker.diagnostics, expr.span, "missing initializer for struct field '%s'", symbol_text(checker, symbol.Id(field.name)))
delete(values, checker.allocator)
return invalid_hir_expr(checker, expr.span, id, struct_type)
}
}
active_field: i64
if union_record {
// The active variant is the one initialized field. Its payload slot
// (`values[0]`) may legitimately be INVALID_EXPR for a void variant, so
// detect "no field" via `initialized`, not the payload value.
found_any := false
for value, index in initialized {
if value {
active_field = i64(index)
found_any = true
break
}
}
if !found_any {
delete(values, checker.allocator)
return invalid_hir_expr(checker, expr.span, source.add(checker.diagnostics, expr.span, "union literal requires a known field"), struct_type)
}
}
return add_hir_expr(checker, hir.Expr{
kind=.Struct, span=expr.span, type=struct_type, args=values, integer=active_field,
target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
case .Keyed:
id := source.add(checker.diagnostics, expr.span, "keyed initializer is only valid inside a struct literal")
return invalid_hir_expr(checker, expr.span, id)
case:
return invalid_hir_expr(checker, expr.span, expr.diagnostic)
}
}
// build_binary_arith constructs the HIR node for `left op right`, where `op` is
// an arithmetic AST kind (`Add`/`Sub`/`Mul`/`Div`). It models many-pointer `+`
// as `Pointer_Add`, coerces both operands to their common type, and emits the
// "arithmetic requires compatible numeric operands" diagnostic when they have no
// shared numeric type.
build_binary_arith :: proc(
checker: ^Checker,
op: ast.Expr_Kind,
left, right: hir.Expr_Id,
span: source.Span,
) -> hir.Expr_Id {
if invalid, propagated := propagate_invalid_expr(checker, span, left, right); propagated {
return invalid
}
// 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,
})
}
operation, ok := numeric_operation_type(
checker,
checker.module.exprs[left].type,
checker.module.exprs[right].type,
)
if !ok || types.is_bool(operation.representation) {
id := source.add(checker.diagnostics, span, "arithmetic requires compatible numeric operands")
return invalid_hir_expr(checker, span, id)
}
if op == .Div && !types.is_float(operation.representation, checker.target) {
id := source.add(
checker.diagnostics, span,
"integer '/' is not allowed; use divtrunc!, divfloor!, divexact!, or divceil!",
)
return invalid_hir_expr(checker, span, id, operation.result)
}
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, operation.result, checker.module.exprs[left].span)
coerced_right := coerce_expr(checker, right, operation.result, checker.module.exprs[right].span)
return add_hir_expr(checker, hir.Expr{
kind=result_kind, span=span, type=operation.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),
numeric_operation := false,
) -> hir.Expr_Id {
stack := checker.build_stack
checker.build_stack = nil
clear_dynamic_array(&stack)
defer {
for frame in stack {
delete(frame.built_args, checker.allocator)
delete(frame.arg_types, checker.allocator)
delete(frame.mapping, checker.allocator)
}
clear_dynamic_array(&stack)
if checker.build_stack == nil {
checker.build_stack = stack
} else {
delete(stack)
}
}
append(&stack, Build_Expr_Frame{
expr=expr_id,
expected=expected,
template=ast.INVALID_FUNCTION,
numeric_operation=numeric_operation,
})
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 {
if expr.kind == .Name || expr.kind == .Field || expr.kind == .Index || expr.kind == .Unwrap {
if specialized, ok := try_build_specialization_expr(
checker, frame.expr, frame.expected, pkg, file,
); ok {
last = specialized
_ = pop(&stack)
continue
}
}
if expr.kind == .Bit_Not || expr.kind == .Bit_And || expr.kind == .Bit_Or || expr.kind == .Bit_Xor ||
expr.kind == .Shift_Left || expr.kind == .Shift_Right || expr.kind == .Shift_Left_Saturating {
if folded, ok := try_fold_typed_integer_expr(checker, frame.expr, frame.expected, pkg, file); ok {
last = folded
_ = pop(&stack)
continue
}
}
constant := Constant{}
_, static_name := current_static_binding(checker, expr.name)
if expr.kind != .Name || symbol.is_valid(expr.qualifier) || !static_name {
constant = eval_constant(checker, frame.expr)
}
if constant.kind == .Value || constant.kind == .Overflow || constant.kind == .Div_By_Zero || constant.kind == .Non_Exact {
last = build_constant_expr(checker, expr, constant, frame.expected, frame.numeric_operation)
_ = pop(&stack)
continue
}
switch expr.kind {
case .String, .Array, .Null, .Unreachable, .Undefined, .Address, .Deref, .Index, .Slice,
.Field, .Unwrap, .Orelse, .Try, .Catch, .Struct_Literal, .Keyed,
.Bool, .Cast, .Comptime, .Not, .Bit_Not, .Bit_And, .Bit_Or, .Bit_Xor, .Shift_Left,
.Shift_Right, .Shift_Left_Saturating, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range,
.Enum_Literal:
last = build_compound_expr(
checker, expr, locals, global_reads, calls, frame.expected, pkg, file,
)
_ = pop(&stack)
case .Function_Literal:
template := ast.Function_Id(u32(expr.integer))
last = build_function_value(checker, template, expr.span, frame.expected)
_ = pop(&stack)
case .Type, .Anonymous_Struct_Type:
id := source.add(checker.diagnostics, expr.span, "type is not a runtime value")
last = invalid_hir_expr(checker, expr.span, id)
_ = pop(&stack)
case .Invalid, .Inference_Hole, .Integer:
last = invalid_hir_expr(checker, expr.span, expr.diagnostic)
_ = pop(&stack)
case .Float:
last = build_float_expr(checker, expr, frame.expected, frame.numeric_operation)
_ = pop(&stack)
case .Name:
last = hir.INVALID_EXPR
if !symbol.is_valid(expr.qualifier) {
if local, ok := find_build_local(locals, expr.name); ok {
last = build_local_expr(checker, local, expr.span)
}
} else if local, ok := find_build_local(locals, expr.qualifier); ok {
base := build_local_expr(checker, local, expr.span)
base_type := local.type
item, has_item := types.container(base_type, &checker.module.types)
field_name := symbol_text(checker, expr.name)
if has_item && (item.kind == .Array || item.kind == .Slice) {
if field_name == "len" {
last = add_hir_expr(checker, hir.Expr{
kind=.Length, span=expr.span, type=types.USIZE, left=base,
target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC,
})
} else if field_name == "ptr" &&
(item.kind == .Slice || types.is_pointer(base_type, &checker.module.types)) {
last = add_hir_expr(checker, hir.Expr{
kind=.Slice_Ptr, span=expr.span,
type=container_pointer_type(&checker.module.types, item), left=base,
target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC,
})
} else if field_name == "ptr" && item.kind == .Array {
id := source.add(checker.diagnostics, expr.span, "arrays do not expose '.ptr'; take their address first")
last = invalid_hir_expr(checker, expr.span, id)
}
}
if types.is_pointer(base_type, &checker.module.types) {
base_type = types.child_type(base_type, &checker.module.types)
}
index, field, found := find_struct_field(checker, base_type, expr.name)
if last == hir.INVALID_EXPR && found {
last = add_hir_expr(checker, hir.Expr{
kind=.Field, span=expr.span, type=field.type, integer=i64(index), left=base,
target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC,
})
}
}
if last == hir.INVALID_EXPR && symbol.is_valid(expr.qualifier) {
if value, ok := current_comptime_value(checker, expr.qualifier);
ok && value.kind == .Static && value.static_value != INVALID_CT_VALUE &&
int(value.static_value) < len(checker.static_state.values) {
if field_value, found := persistent_field_value(checker, value.static_value, expr.name); found {
last = build_static_value(checker, field_value, expr.span, frame.expected)
}
}
}
if last == hir.INVALID_EXPR && symbol.is_valid(expr.qualifier) {
if value, ok := static_field_value(checker, expr.qualifier, expr.name); ok {
last = build_static_value(checker, value, expr.span, frame.expected)
}
}
if last == hir.INVALID_EXPR && symbol.is_valid(expr.qualifier) &&
find_import(checker, file, expr.qualifier) == ast.INVALID_IMPORT {
if global := find_global(checker, expr.qualifier, pkg, file); global != ast.INVALID_GLOBAL {
base := build_global_reference(checker, global, expr.span, global_reads)
base_type := checker.global_types[global]
item, has_item := types.container(base_type, &checker.module.types)
field_name := symbol_text(checker, expr.name)
if has_item && (item.kind == .Array || item.kind == .Slice) {
if field_name == "len" {
last = add_hir_expr(checker, hir.Expr{
kind=.Length, span=expr.span, type=types.USIZE, left=base,
target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC,
})
} else if field_name == "ptr" &&
(item.kind == .Slice || types.is_pointer(base_type, &checker.module.types)) {
last = add_hir_expr(checker, hir.Expr{
kind=.Slice_Ptr, span=expr.span,
type=container_pointer_type(&checker.module.types, item), left=base,
target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC,
})
} else if field_name == "ptr" && item.kind == .Array {
id := source.add(checker.diagnostics, expr.span, "arrays do not expose '.ptr'; take their address first")
last = invalid_hir_expr(checker, expr.span, id)
}
}
if types.is_pointer(base_type, &checker.module.types) {
base_type = types.child_type(base_type, &checker.module.types)
}
index, field, found := find_struct_field(checker, base_type, expr.name)
if last == hir.INVALID_EXPR && found {
last = add_hir_expr(checker, hir.Expr{
kind=.Field, span=expr.span, type=field.type, integer=i64(index), left=base,
target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC,
})
}
}
}
if last == hir.INVALID_EXPR {
if !symbol.is_valid(expr.qualifier) {
if binding, ok := current_static_binding(checker, expr.name); ok &&
binding.value != INVALID_CT_VALUE && int(binding.value) < len(checker.static_state.values) {
last = build_static_value(checker, checker.static_state.values[binding.value], expr.span, frame.expected)
}
if last == hir.INVALID_EXPR {
if value, ok := current_comptime_value(checker, expr.name); ok {
if value.kind == .Integer {
expected_type := value.type
if types.is_concrete_integer(frame.expected) || types.is_float(frame.expected, checker.target) {
expected_type = frame.expected
}
last = build_constant_expr(
checker,
expr,
Constant{kind=.Value, value=value.value},
expected_type,
)
} else if value.kind == .String {
string_id := u64(0)
for text, index in checker.ast_module.strings {
if text == value.text {
string_id = u64(index)
break
}
}
constant_expr := expr
constant_expr.kind = .String
constant_expr.integer = string_id
last = build_compound_expr(
checker, constant_expr, locals, global_reads, calls,
frame.expected, pkg, file,
)
} else if value.kind == .Static && value.static_value != INVALID_CT_VALUE &&
int(value.static_value) < len(checker.static_state.values) {
last = build_static_value(
checker, checker.static_state.values[value.static_value], expr.span, frame.expected,
)
} else {
id := source.addf(
checker.diagnostics,
expr.span,
"type parameter '%s' is not a runtime value",
symbol_text(checker, expr.name),
)
last = invalid_hir_expr(checker, expr.span, id)
}
}
}
}
}
if last == hir.INVALID_EXPR {
if enum_type, enum_ok := enum_type_from_name_expr(checker, expr, pkg, file); enum_ok {
last = enum_member_hir(checker, enum_type, expr.name, expr.span)
}
}
if last == hir.INVALID_EXPR {
target_pkg, available := expr_package(checker, expr, pkg, file, true)
if !available {
id := add_package_resolution_diagnostic(checker, expr, file)
last = invalid_hir_expr(checker, expr.span, id)
} else if global := find_global(checker, expr.name, target_pkg, expr_lookup_file(expr, file)); global != ast.INVALID_GLOBAL {
last = build_global_reference(checker, global, expr.span, global_reads)
} else {
template := find_template(checker, expr.name, target_pkg, expr_lookup_file(expr, file))
if template != ast.INVALID_FUNCTION {
last = build_function_value(checker, template, expr.span, frame.expected)
} else {
id := add_unsupported_diagnostic(checker, expr.span, target_pkg, expr.name)
if id == source.INVALID_DIAGNOSTIC {
id = add_name_resolution_diagnostic(checker, expr, target_pkg, file)
}
last = invalid_hir_expr(checker, expr.span, id)
}
}
}
_ = pop(&stack)
case .Negate:
stack[frame_index].stage = 5
append(&stack, Build_Expr_Frame{expr=expr.left, expected=types.INVALID, template=ast.INVALID_FUNCTION})
case .Add, .Sub, .Mul, .Div:
stack[frame_index].stage = 1
left_const := is_numeric_constant_expr(checker, expr.left)
right_const := is_numeric_constant_expr(checker, expr.right)
stack[frame_index].reverse_operands = left_const && !right_const
first := expr.right if stack[frame_index].reverse_operands else expr.left
first_expected := types.INVALID
if !stack[frame_index].reverse_operands &&
types.is_concrete_scalar(frame.expected) && !types.is_bool(frame.expected) {
first_expected = frame.expected
}
append(&stack, Build_Expr_Frame{
expr=first,
expected=first_expected,
template=ast.INVALID_FUNCTION,
})
case .Call:
if expr.left != ast.INVALID_EXPR {
stack[frame_index].stage = 6
append(&stack, Build_Expr_Frame{expr=expr.left, expected=types.INVALID, template=ast.INVALID_FUNCTION})
continue
}
if builtin := type_builtin_call(checker, expr); builtin != .None {
last = build_type_builtin(checker, expr, builtin, pkg, file)
_ = pop(&stack)
continue
}
if builtin := division_builtin_call(checker, expr); builtin != .None {
last = build_division_builtin(
checker, expr, builtin, locals, global_reads, calls, frame.expected, pkg, file,
)
_ = pop(&stack)
continue
}
if builtin := memory_builtin_call(checker, expr); builtin != .None {
last = build_memory_builtin(checker, expr, builtin, locals, global_reads, calls, pkg, file)
_ = pop(&stack)
continue
}
if is_intrinsic_call(checker, expr, "some") {
if len(expr.args) != 1 {
id := source.addf(checker.diagnostics, expr.span, "some! expects 1 argument, got %d", len(expr.args))
last = invalid_hir_expr(checker, expr.span, id)
_ = pop(&stack)
continue
}
if !types.is_optional(frame.expected, &checker.module.types) {
id := source.add(checker.diagnostics, expr.span, "some! requires an optional context")
last = invalid_hir_expr(checker, expr.span, id)
_ = pop(&stack)
continue
}
child_type := types.child_type(frame.expected, &checker.module.types)
child := build_nested_expr(checker, expr.args[0], locals, global_reads, calls, child_type, pkg, file)
child = coerce_expr(checker, child, child_type, checker.ast_module.exprs[expr.args[0]].span)
last = add_hir_expr(checker, hir.Expr{
kind=.Optional_Some, span=expr.span, type=frame.expected, left=child,
target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC,
})
_ = pop(&stack)
continue
}
if field_expr, handled := field_intrinsic_expr(checker, expr, pkg, file); handled {
if enum_type, ok := resolve_type_argument(checker, field_expr.left, pkg, file);
ok && types.is_enum(enum_type, &checker.module.types) {
last = enum_member_hir(checker, enum_type, field_expr.name, field_expr.span)
_ = pop(&stack)
continue
}
base := build_nested_expr(
checker, field_expr.left, locals, global_reads, calls,
types.INVALID, pkg, file,
)
last, _ = build_field_from_value(
checker, field_expr, base, checker.module.exprs[base].type,
)
_ = pop(&stack)
continue
}
if is_intrinsic_call(checker, expr, "typeinfo") {
id := source.add(
checker.diagnostics, expr.span,
"typeinfo! produces compile-time-only metadata and cannot be stored or passed at runtime",
)
last = invalid_hir_expr(checker, expr.span, id)
_ = pop(&stack)
continue
}
if is_intrinsic_call(checker, expr, "tag") {
last = build_tag_intrinsic(checker, expr, locals, global_reads, calls, pkg, file)
_ = pop(&stack)
continue
}
if is_intrinsic_call(checker, expr, "tagname") {
last = build_tagname_intrinsic(checker, expr, pkg, file)
_ = pop(&stack)
continue
}
if is_ptrcast_call(checker, expr) {
if len(expr.args) != 2 {
id := source.addf(checker.diagnostics, expr.span, "ptrcast! expects 2 arguments, got %d", len(expr.args))
last = invalid_hir_expr(checker, expr.span, id)
_ = pop(&stack)
continue
}
target, target_ok := resolve_type_argument(checker, expr.args[0], pkg, file)
if !target_ok {
id := source.add(checker.diagnostics, checker.ast_module.exprs[expr.args[0]].span, "ptrcast! target must be a type")
last = invalid_hir_expr(checker, expr.span, id)
_ = pop(&stack)
continue
}
if !valid_ptrcast_child(checker, target) {
id := source.addf(checker.diagnostics, checker.ast_module.exprs[expr.args[0]].span, "ptrcast! target must be a sized runtime object type, got %s", type_label(checker, target))
last = invalid_hir_expr(checker, expr.span, id)
_ = pop(&stack)
continue
}
stack[frame_index].target_type = target
stack[frame_index].stage = 9
append(&stack, Build_Expr_Frame{expr=expr.args[1], expected=types.INVALID, template=ast.INVALID_FUNCTION})
continue
}
if is_intrinsic_call(checker, expr, "constcast") {
if len(expr.args) != 1 {
id := source.addf(checker.diagnostics, expr.span, "constcast! expects 1 argument, got %d", len(expr.args))
last = invalid_hir_expr(checker, expr.span, id)
_ = pop(&stack)
continue
}
stack[frame_index].stage = 10
append(&stack, Build_Expr_Frame{expr=expr.args[0], expected=types.INVALID, template=ast.INVALID_FUNCTION})
continue
}
if is_intrinsic_call(checker, expr, "compile_error") {
message := "compile_error! requires one comptime string argument"
if len(expr.args) == 1 {
if text, ok := comptime_string_argument(checker, expr.args[0], pkg, file); ok {
message = text
}
}
id := source.add(checker.diagnostics, expr.span, message)
last = invalid_hir_expr(checker, expr.span, id, types.VOID)
_ = pop(&stack)
continue
}
if expr.intrinsic {
id := source.INVALID_DIAGNOSTIC
if symbol.is_valid(expr.qualifier) {
id = source.add(checker.diagnostics, expr.span, "intrinsic calls must be unqualified")
} else {
id = source.addf(checker.diagnostics, expr.span, "unknown intrinsic '%s!'", symbol_text(checker, expr.name))
}
last = invalid_hir_expr(checker, expr.span, id)
_ = pop(&stack)
continue
}
if callee, handled, ok := build_qualified_value_field(checker, expr, locals, global_reads, pkg, file); handled {
if !ok {
last = callee
_ = pop(&stack)
continue
}
stack[frame_index].stage = 6
last = callee
continue
}
target_pkg, available := expr_package(checker, expr, pkg, file, true)
if !available {
id := add_package_resolution_diagnostic(checker, expr, file)
last = invalid_hir_expr(checker, expr.span, id)
_ = pop(&stack)
continue
}
template := find_template(checker, expr.name, target_pkg, expr_lookup_file(expr, file))
if template == ast.INVALID_FUNCTION {
callee := hir.INVALID_EXPR
non_callable := false
non_callable_global := false
if !symbol.is_valid(expr.qualifier) {
if local, ok := find_build_local(locals, expr.name); ok {
if _, _, _, callable := types.callable_function(local.type, &checker.module.types); callable {
callee = build_local_expr(checker, local, expr.span)
} else {
non_callable = true
}
}
if callee == hir.INVALID_EXPR && !non_callable {
if binding, ok := current_static_binding(checker, expr.name); ok &&
binding.value != INVALID_CT_VALUE && int(binding.value) < len(checker.static_state.values) {
callee = build_static_value(
checker, checker.static_state.values[binding.value], expr.span, binding.type,
)
}
}
if callee == hir.INVALID_EXPR && !non_callable {
if value, ok := current_comptime_value(checker, expr.name);
ok && value.kind == .Static && value.static_value != INVALID_CT_VALUE &&
int(value.static_value) < len(checker.static_state.values) {
callee = build_static_value(
checker, checker.static_state.values[value.static_value], expr.span, types.INVALID,
)
}
}
}
if callee == hir.INVALID_EXPR && !non_callable {
if global := find_global(checker, expr.name, target_pkg, expr_lookup_file(expr, file)); global != ast.INVALID_GLOBAL {
if _, _, _, callable := types.callable_function(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 {
named_type := types.find_named(&checker.module.types, u32(target_pkg), u32(expr.name), file=u32(expr_lookup_file(expr, file)))
named_item, named_ok := types.node(&checker.module.types, named_type)
constructor_type := types.resolve_alias(named_type, &checker.module.types)
constructor_item, constructor_ok := types.node(&checker.module.types, constructor_type)
scalar_alias := named_ok && named_item.kind == .Alias &&
types.is_concrete_scalar(constructor_type)
if scalar_alias {
if len(expr.args) != 1 {
id := source.addf(
checker.diagnostics,
expr.span,
"type alias '%s' expects 1 argument, got %d",
symbol_text(checker, expr.name),
len(expr.args),
)
last = invalid_hir_expr(checker, expr.span, id, constructor_type)
_ = pop(&stack)
continue
}
stack[frame_index].target_type = constructor_type
stack[frame_index].stage = 11
append(&stack, Build_Expr_Frame{
expr=expr.args[0],
expected=types.INVALID,
template=ast.INVALID_FUNCTION,
})
continue
}
if constructor_ok && constructor_item.kind == .Distinct {
if !is_runtime_type(checker, constructor_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, constructor_type)
_ = pop(&stack)
continue
}
stack[frame_index].target_type = constructor_type
stack[frame_index].stage = 8
append(&stack, Build_Expr_Frame{
expr=expr.args[0],
expected=constructor_item.child,
template=ast.INVALID_FUNCTION,
})
continue
}
id := source.INVALID_DIAGNOSTIC
if non_callable {
id = add_call_resolution_diagnostic(checker, expr, target_pkg, file) if non_callable_global else
source.add(checker.diagnostics, expr.span, "call target is not callable")
} else {
id = add_unsupported_diagnostic(checker, expr.span, target_pkg, expr.name)
if id == source.INVALID_DIAGNOSTIC {
id = add_call_resolution_diagnostic(checker, expr, target_pkg, file)
}
}
last = invalid_hir_expr(checker, expr.span, id)
_ = pop(&stack)
continue
}
_, function_item, function_type, _ := types.callable_function(checker.module.exprs[callee].type, &checker.module.types)
if !valid_callable_arity(function_item, len(expr.args)) {
message := "function expects at least %d arguments, got %d" if function_item.variadic else
"function expects %d arguments, got %d"
id := source.addf(checker.diagnostics, expr.span, message, function_item.field_count, len(expr.args))
last = invalid_hir_expr(checker, expr.span, id)
_ = pop(&stack)
continue
}
stack[frame_index].left = callee
stack[frame_index].built_args = make([]hir.Expr_Id, len(expr.args), checker.allocator)
stack[frame_index].stage = 7
if len(expr.args) > 0 {
arg_expected := callable_arg_expected(function_type, function_item, &checker.module.types, 0)
if !is_runtime_type(checker, arg_expected) {
arg_expected = types.INVALID
}
append(&stack, Build_Expr_Frame{expr=expr.args[0], expected=arg_expected, template=ast.INVALID_FUNCTION})
}
continue
}
if len(checker.ast_module.functions[template].unsupported_reason) > 0 {
id := source.addf(
checker.diagnostics,
expr.span,
"C declaration '%s' is unavailable: %s",
symbol_text(checker, expr.name),
checker.ast_module.functions[template].unsupported_reason,
)
last = invalid_hir_expr(checker, expr.span, id)
_ = pop(&stack)
continue
}
if checker.template_diagnostics[template] != source.INVALID_DIAGNOSTIC {
last = invalid_hir_expr(checker, expr.span, checker.template_diagnostics[template])
_ = pop(&stack)
continue
}
function := checker.ast_module.functions[template]
if is_type_metatype_syntax(checker, function.result) {
id := source.addf(checker.diagnostics, expr.span, "type factory '%s' is only valid in type position", symbol_text(checker, expr.name))
last = invalid_hir_expr(checker, expr.span, id)
_ = pop(&stack)
continue
}
resolution_index, resolved := find_call_resolution(checker, frame.expr)
mapping: []int
mode := Call_Argument_Mode.Invalid
prefix := comptime_param_count(function)
mapping_failure := ""
if resolved {
mapping = slice.clone(checker.call_resolutions[resolution_index].mapping, checker.allocator)
mode = call_mapping_mode(function, mapping)
} else {
infer_locals := make([]Infer_Local, len(locals), checker.allocator)
for local, index in locals {
infer_locals[index] = Infer_Local{name=local.name, type=local.type, declared=local.type}
}
mapping, mode, prefix, mapping_failure = call_argument_mapping(
checker, &function, expr.args, pkg, file, frame.expected, infer_locals,
)
delete(infer_locals, checker.allocator)
}
if mode == .Invalid {
id := source.INVALID_DIAGNOSTIC
if !function_has_comptime_params(function) {
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),
)
if !function.imported {
source.add_secondary_label(checker.diagnostics, id, function.span, "function declared here")
}
} else if len(mapping_failure) > 0 {
id = source.addf(
checker.diagnostics, expr.span,
"call to '%s' has no unique complete argument mapping: %s",
symbol_text(checker, expr.name), mapping_failure,
)
} else {
id = source.addf(
checker.diagnostics, expr.span,
"call to '%s' has no unique complete argument mapping",
symbol_text(checker, expr.name),
)
}
delete(mapping_failure, checker.allocator)
last = invalid_hir_expr(checker, expr.span, id)
_ = pop(&stack)
continue
}
stack[frame_index].template = template
stack[frame_index].arg_mode = mode
stack[frame_index].prefix = prefix
stack[frame_index].resolution = resolution_index if resolved else -1
stack[frame_index].mapping = mapping
stack[frame_index].built_args = make([]hir.Expr_Id, len(expr.args), checker.allocator)
stack[frame_index].arg_types = make([]types.Type, len(function.params), checker.allocator)
for &arg in stack[frame_index].built_args {
arg = hir.INVALID_EXPR
}
stack[frame_index].arg_index = next_runtime_call_arg(function, mapping, 0, len(expr.args))
stack[frame_index].stage = 3
if stack[frame_index].arg_index < len(expr.args) {
param_index := call_param_index(mapping, stack[frame_index].arg_index)
arg_expected := resolved_call_arg_expected(
checker, function, param_index, stack[frame_index].resolution,
)
if !is_runtime_type(checker, arg_expected) {
arg_expected = types.INVALID
}
append(&stack, Build_Expr_Frame{
expr=expr.args[stack[frame_index].arg_index],
expected=arg_expected,
template=ast.INVALID_FUNCTION,
})
}
}
continue
}
if frame.stage == 5 {
operand := last
if invalid, propagated := propagate_invalid_expr(checker, expr.span, operand); propagated {
last = invalid
_ = pop(&stack)
continue
}
operand_type := checker.module.exprs[operand].type
representation := types.runtime_representation(operand_type, &checker.module.types)
if !types.is_signed(representation, checker.target) && !types.is_float(representation, 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,
})
expected_representation := types.runtime_representation(frame.expected, &checker.module.types)
if types.equal(operand_type, frame.expected) &&
(types.is_signed(expected_representation, checker.target) ||
types.is_float(expected_representation, 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
first_type := checker.module.exprs[last].type
second := expr.left if frame.reverse_operands else expr.right
second_expected := types.INVALID
numeric_operation := false
if types.is_many_pointer(first_type, &checker.module.types) {
second_expected = types.USIZE
} else if is_numeric_constant_expr(checker, second) {
second_expected = first_type
if representation, ok := types.distinct_scalar_backing(first_type, &checker.module.types);
ok && !types.is_bool(representation) {
numeric_operation = true
}
} else if types.is_concrete_scalar(frame.expected) && !types.is_bool(frame.expected) {
second_expected = frame.expected
}
append(&stack, Build_Expr_Frame{
expr=second,
expected=second_expected,
template=ast.INVALID_FUNCTION,
numeric_operation=numeric_operation,
})
continue
}
if frame.stage == 2 {
first, second := frame.left, last
left, right := first, second
if frame.reverse_operands {
left, right = second, first
}
last = build_binary_arith(checker, expr.kind, left, right, expr.span)
_ = pop(&stack)
continue
}
if frame.stage == 3 {
if frame.arg_index < len(expr.args) {
stack[frame_index].built_args[frame.arg_index] = last
param_index := call_param_index(frame.mapping, frame.arg_index)
if param_index < len(stack[frame_index].arg_types) {
stack[frame_index].arg_types[param_index] = checker.module.exprs[last].type
}
next := next_runtime_call_arg(
checker.ast_module.functions[frame.template], frame.mapping,
frame.arg_index+1, len(expr.args),
)
stack[frame_index].arg_index = next
if next < len(expr.args) {
next_param := call_param_index(frame.mapping, next)
arg_expected := resolved_call_arg_expected(
checker, checker.ast_module.functions[frame.template], next_param,
frame.resolution,
)
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
}
}
if invalid, propagated := propagate_invalid_expr(checker, expr.span, ..stack[frame_index].built_args); propagated {
delete(stack[frame_index].arg_types, checker.allocator)
stack[frame_index].arg_types = nil
delete(stack[frame_index].built_args, checker.allocator)
stack[frame_index].built_args = nil
delete(stack[frame_index].mapping, checker.allocator)
stack[frame_index].mapping = nil
last = invalid
_ = pop(&stack)
continue
}
function := checker.ast_module.functions[frame.template]
comptime_values: []Comptime_Value
comptime_ok := false
if frame.resolution >= 0 && frame.resolution < len(checker.call_resolutions) {
comptime_values = clone_comptime_values(
checker.call_resolutions[frame.resolution].comptime_values,
checker.allocator,
)
comptime_ok = true
} else {
comptime_values, comptime_ok = infer_call_comptime_values(
checker, function, frame.prefix, frame.mapping, expr.args, stack[frame_index].arg_types,
frame.expected, pkg, file, diagnose=true,
)
}
defer delete(comptime_values, checker.allocator)
if comptime_ok {
previous_comptime := checker.current_comptime_values
checker.current_comptime_values = comptime_values
for source_index in 0..<len(expr.args) {
param_index := call_param_index(frame.mapping, source_index)
if param_index >= len(function.params) || function.params[param_index].comptime_value ||
!is_numeric_constant_expr(checker, expr.args[source_index]) {
continue
}
expected_arg := call_arg_expected(checker, function, param_index)
if !is_runtime_type(checker, expected_arg) {
continue
}
rebuilt := build_nested_expr(
checker, expr.args[source_index], locals, global_reads, calls,
expected_arg, pkg, file,
)
stack[frame_index].built_args[source_index] = rebuilt
stack[frame_index].arg_types[param_index] = checker.module.exprs[rebuilt].type
}
checker.current_comptime_values = previous_comptime
}
arg_violation := source.INVALID_DIAGNOSTIC
if comptime_ok {
previous_comptime := checker.current_comptime_values
checker.current_comptime_values = comptime_values
params := function.params
for index in 0..<len(params) {
if index >= len(stack[frame_index].arg_types) {
break
}
if params[index].comptime_value {
continue
}
declared := type_from_syntax(checker, params[index].type, function.pkg, function.file)
actual := stack[frame_index].arg_types[index]
if types.is_constraint(declared) && types.is_valid(actual) &&
!types.is_valid(types.constraint_target(declared, actual, &checker.module.types)) {
arg_violation = source.addf(
checker.diagnostics, expr.span,
"cannot pass %s to '%s' parameter '%s'",
types.name(actual), types.name(declared),
symbol_text(checker, params[index].name),
)
break
}
}
checker.current_comptime_values = previous_comptime
}
fold := Call_Fold.Runtime
if comptime_ok && can_fold_zero_runtime_call(checker, function) &&
frame.resolution >= 0 && frame.resolution < len(checker.call_resolutions) {
fold = cache_zero_runtime_call(checker, frame.resolution, frame.expr, pkg, file)
}
if comptime_ok && arg_violation == source.INVALID_DIAGNOSTIC &&
(fold == .Value || fold == .Compile_Error) {
if fold == .Value {
value := checker.call_resolutions[frame.resolution].folded_value
last = ct_materialize_value(&checker.static_state, value, expr.span, frame.expected)
} else {
diagnostic := checker.call_resolutions[frame.resolution].diagnostic
last = invalid_hir_expr(checker, expr.span, diagnostic, frame.expected)
}
delete(stack[frame_index].arg_types, checker.allocator)
stack[frame_index].arg_types = nil
delete(stack[frame_index].built_args, checker.allocator)
stack[frame_index].built_args = nil
delete(stack[frame_index].mapping, checker.allocator)
stack[frame_index].mapping = nil
_ = pop(&stack)
continue
}
spec := INVALID_SPEC
if comptime_ok {
if frame.resolution >= 0 && frame.resolution < len(checker.call_resolutions) {
spec = find_spec(
checker, frame.template,
checker.call_resolutions[frame.resolution].runtime_types,
comptime_values,
)
} else {
spec = find_spec(checker, frame.template, stack[frame_index].arg_types, comptime_values)
}
}
delete(stack[frame_index].arg_types, checker.allocator)
stack[frame_index].arg_types = nil
if !comptime_ok || arg_violation != source.INVALID_DIAGNOSTIC {
delete(stack[frame_index].built_args, checker.allocator)
stack[frame_index].built_args = nil
delete(stack[frame_index].mapping, checker.allocator)
stack[frame_index].mapping = nil
diagnostic := arg_violation
if diagnostic == source.INVALID_DIAGNOSTIC {
diagnostic = source.add(checker.diagnostics, expr.span, "invalid comptime argument")
}
last = invalid_hir_expr(checker, expr.span, diagnostic)
_ = pop(&stack)
continue
}
if spec == INVALID_SPEC {
id := source.addf(
checker.diagnostics,
expr.span,
"could not resolve specialization of '%s'",
symbol_text(checker, expr.name),
)
delete(stack[frame_index].built_args, checker.allocator)
stack[frame_index].built_args = nil
delete(stack[frame_index].mapping, checker.allocator)
stack[frame_index].mapping = nil
last = invalid_hir_expr(checker, expr.span, id)
_ = pop(&stack)
continue
}
source_args := stack[frame_index].built_args
runtime_count := runtime_param_count(function)
runtime_arg_count := runtime_count + max(0, len(source_args)-len(function.params))
runtime_args := make([]hir.Expr_Id, runtime_arg_count, checker.allocator)
runtime_index := 0
for param, param_index in function.params {
if param.comptime_value {
continue
}
source_index := -1
for mapped_param, mapped_source in frame.mapping {
if mapped_param == param_index {
source_index = mapped_source
break
}
}
assert(source_index >= 0)
arg := source_args[source_index]
runtime_args[runtime_index] = coerce_expr(
checker,
arg,
checker.specs[spec].args[runtime_index],
checker.module.exprs[arg].span,
)
runtime_index += 1
}
for source_index in len(function.params)..<len(source_args) {
arg := source_args[source_index]
runtime_args[runtime_index] = promote_c_vararg_expr(
checker,
arg,
checker.module.exprs[arg].span,
)
runtime_index += 1
}
delete(stack[frame_index].built_args, checker.allocator)
stack[frame_index].built_args = nil
delete(stack[frame_index].mapping, checker.allocator)
stack[frame_index].mapping = nil
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(runtime_args, checker.allocator)
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=runtime_args, diagnostic = source.INVALID_DIAGNOSTIC,
})
}
_ = pop(&stack)
}
if frame.stage == 6 {
callee := last
if invalid, propagated := propagate_invalid_expr(checker, expr.span, callee); propagated {
last = invalid
_ = pop(&stack)
continue
}
_, function_item, function_type, ok := types.callable_function(checker.module.exprs[callee].type, &checker.module.types)
if !ok {
id := source.add(checker.diagnostics, expr.span, "call target is not callable")
last = invalid_hir_expr(checker, expr.span, id)
_ = pop(&stack)
continue
}
if !valid_callable_arity(function_item, len(expr.args)) {
message := "function expects at least %d arguments, got %d" if function_item.variadic else
"function 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.callable_function(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
}
}
invalid, propagated := propagate_invalid_expr(checker, expr.span, frame.left)
if !propagated {
invalid, propagated = propagate_invalid_expr(checker, expr.span, ..stack[frame_index].built_args)
}
if propagated {
delete(stack[frame_index].built_args, checker.allocator)
stack[frame_index].built_args = nil
last = invalid
_ = pop(&stack)
continue
}
callee_type := checker.module.exprs[frame.left].type
_, function_item, function_type, ok := types.callable_function(callee_type, &checker.module.types)
if !ok {
id := source.add(checker.diagnostics, expr.span, "call target is not callable")
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 callable 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 {
target := hir.INVALID_REF
callee := frame.left
callee_expr := checker.module.exprs[frame.left]
if callee_expr.kind == .Function {
function := hir.as_function(callee_expr.target)
if function != hir.INVALID_FUNCTION {
target = callee_expr.target
callee = hir.INVALID_EXPR
add_unique_function(calls, function)
}
}
last = add_hir_expr(checker, hir.Expr{
kind=.Call, span=expr.span, type=result, target=target,
left=callee, 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.can_retype_distinct(actual, frame.target_type, &checker.module.types) {
backing := types.resolve_alias(distinct_item.child, &checker.module.types)
if types.is_concrete_scalar(backing) && !types.is_bool(backing) {
last = build_scalar_cast(checker, last, backing, expr.span)
actual = checker.module.exprs[last].type
}
}
if !ok || distinct_item.kind != .Distinct ||
!types.can_retype_distinct(actual, frame.target_type, &checker.module.types) {
id := source.addf(
checker.diagnostics,
expr.span,
"cannot construct distinct type '%s' from %s",
symbol_text(checker, expr.name),
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)
}
if frame.stage == 9 {
result, ok := types.replace_pointer_child(&checker.module.types, checker.module.exprs[last].type, frame.target_type)
if !ok {
id := source.add(checker.diagnostics, expr.span, "ptrcast! operand must be a pointer or optional pointer")
last = invalid_hir_expr(checker, expr.span, id)
} else {
last = add_hir_expr(checker, hir.Expr{
kind=.Pointer_Cast,
span=expr.span,
type=result,
left=last,
target=hir.INVALID_REF,
right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
_ = pop(&stack)
}
if frame.stage == 10 {
result, ok := types.restore_mutability(&checker.module.types, checker.module.exprs[last].type)
if !ok {
id := source.add(checker.diagnostics, expr.span, "constcast! operand must be a pointer, optional pointer, or slice")
last = invalid_hir_expr(checker, expr.span, id)
} else {
last = add_hir_expr(checker, hir.Expr{
kind=.Const_Cast,
span=expr.span,
type=result,
left=last,
target=hir.INVALID_REF,
right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
_ = pop(&stack)
}
if frame.stage == 11 {
last = build_scalar_cast(checker, last, frame.target_type, expr.span)
_ = 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]
native_main := function.pkg == 0 && function.name == checker.main_symbol &&
checker.entry_point == .Plain &&
types.kind(spec.result, &checker.module.types) != .Fallible
if native_main {
return fmt.aprintf("main", allocator = checker.allocator)
}
if function.generated {
return fmt.aprintf("bro__p%d__anon%d", function.pkg, spec.template, allocator = checker.allocator)
}
if !function.has_body && function.c_abi {
if len(function.link_name) > 0 {
return strings.clone(function.link_name, checker.allocator)
}
return fmt.aprintf("%s", symbol_text(checker, function.name), allocator = checker.allocator)
}
builder := strings.builder_make(checker.allocator)
defer strings.builder_destroy(&builder)
strings.write_string(&builder, "bro_c__" if function.c_abi else "bro__")
fmt.sbprintf(&builder, "p%d__", function.pkg)
strings.write_string(&builder, symbol_text(checker, function.name))
for arg in spec.args {
strings.write_string(&builder, "__")
if arg >= types.DYNAMIC_START {
fmt.sbprintf(&builder, "t%d", arg)
} else {
strings.write_string(&builder, types.name(arg))
}
}
for value in spec.comptime_values {
if value.kind == .Type {
strings.write_string(&builder, "__ct")
if value.type >= types.DYNAMIC_START {
fmt.sbprintf(&builder, "t%d", value.type)
} else {
strings.write_string(&builder, types.name(value.type))
}
} else if value.kind == .String {
fmt.sbprintf(&builder, "__cs%d_", len(value.text))
hex := "0123456789abcdef"
for byte in transmute([]byte)value.text {
strings.write_byte(&builder, hex[byte>>4])
strings.write_byte(&builder, hex[byte&0xf])
}
} else if value.kind == .Static {
fmt.sbprintf(&builder, "__ca%d_%016x", len(value.key), value.fingerprint)
} else {
strings.write_string(&builder, "__cv")
if value.value < 0 {
strings.write_string(&builder, "n")
fmt.sbprintf(&builder, "%d", -value.value)
} else {
fmt.sbprintf(&builder, "%d", value.value)
}
}
}
return fmt.aprintf("%s", strings.to_string(builder), allocator = checker.allocator)
}
// Replay eligible cleanup in `[lo, len(defers))` in LIFO order. Error exits run
// both defer forms; other exits skip errdefer. A direct error return supplies its
// preserved payload so captured errors can be initialized before each cleanup.
flush_defers :: proc(
ctx: ^Build_Ctx,
body: ^[dynamic]hir.Stmt_Id,
lo: int,
error_exit := false,
error_value := hir.INVALID_EXPR,
) {
for i := len(ctx.defers^) - 1; i >= lo; i -= 1 {
entry := ctx.defers^[i]
if entry.error_only && !error_exit {
continue
}
if error_exit && entry.capture != hir.INVALID_LOCAL && error_value != hir.INVALID_EXPR {
append(body, hir.stmt_id(len(ctx.checker.module.statements)))
append(&ctx.checker.module.statements, hir.Stmt{
kind=.Declaration, local=entry.capture, expr=error_value,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
for stmt_id in entry.body {
append(body, stmt_id)
}
}
}
// Copy the active error-exit cleanup onto a Try expression. Capture locals are
// initialized by lowering once the propagated error has been extracted/widened.
try_cleanup :: proc(ctx: ^Build_Ctx) -> ([]hir.Stmt_Id, []hir.Expr_Id) {
body: [dynamic]hir.Stmt_Id
body.allocator = ctx.checker.allocator
captures: [dynamic]hir.Expr_Id
captures.allocator = ctx.checker.allocator
for i := len(ctx.defers^) - 1; i >= 0; i -= 1 {
entry := ctx.defers^[i]
if entry.error_only && entry.capture != hir.INVALID_LOCAL {
append(&captures, hir.Expr_Id(entry.capture))
}
append(&body, ..entry.body)
}
return body[:], captures[:]
}
specialization_bool :: proc(checker: ^Checker, expr: ast.Expr_Id, pkg: ast.Package_Id, file: ast.File_Id) -> (bool, bool) {
if len(checker.current_comptime_values) == 0 && len(checker.static_bindings) == 0 {
return false, false
}
state := ct_state_make(checker, pkg, file, values=checker.current_comptime_values, diagnose=false)
defer ct_state_destroy(&state)
value, flow, ok := ct_eval_expr(&state, expr, types.BOOL, 0)
if !ok || flow.kind != .Normal {
return false, false
}
return ct_bool_value(&state, value)
}
specialization_match_body :: proc(
checker: ^Checker,
statement: ast.Stmt,
pkg: ast.Package_Id,
file: ast.File_Id,
) -> ([]ast.Stmt_Id, Static_Binding, bool, bool) {
if len(checker.current_comptime_values) == 0 && len(checker.static_bindings) == 0 {
return nil, {}, false, false
}
state := ct_state_make(checker, pkg, file, values=checker.current_comptime_values, diagnose=false)
defer ct_state_destroy(&state)
subject, flow, ok := ct_eval_expr(&state, statement.expr, types.INVALID, 0)
if !ok || flow.kind != .Normal || subject == INVALID_CT_VALUE || int(subject) >= len(state.values) {
return nil, {}, false, false
}
selection, selected := ct_select_match_arm(&state, statement, subject, 0)
if !selected {
return nil, {}, false, false
}
arm := checker.ast_module.statements[selection.arm]
if arm.expand {
return nil, {}, false, false
}
if arm.pointer_capture {
return nil, {}, false, false
}
if len(arm.captures) > 0 {
if selection.payload == INVALID_CT_VALUE || types.is_void(selection.payload_type) {
return nil, {}, false, false
}
if arm.captures[0] != checker.sink_symbol {
return arm.body, store_static_binding(checker, &state, selection.payload, arm.captures[0]), true, true
}
}
return arm.body, {}, false, true
}
Expand_Binding_Error :: enum u8 {
None,
Invalid,
Quota,
Diagnosed,
}
expand_field_bindings :: proc(
checker: ^Checker,
expr: ast.Expr_Id,
capture: symbol.Id,
pkg: ast.Package_Id,
file: ast.File_Id,
diagnose := false,
) -> ([]Static_Binding, Expand_Binding_Error) {
state := ct_state_make(checker, pkg, file, values=checker.current_comptime_values, diagnose=diagnose)
defer ct_state_destroy(&state)
value_id, flow, ok := ct_eval_expr(&state, expr, types.INVALID, 0)
if !ok || flow.kind != .Normal || value_id == INVALID_CT_VALUE || int(value_id) >= len(state.values) {
return nil, .Diagnosed if state.diagnostic != source.INVALID_DIAGNOSTIC else .Invalid
}
value := state.values[value_id]
if ct_value_contains_undefined(&state, value_id) {
if diagnose {
_ = ct_fail(&state, .Not_Comptime, checker.ast_module.exprs[expr].span, "inline for cannot iterate an undefined comptime value")
}
return nil, .Diagnosed if state.diagnostic != source.INVALID_DIAGNOSTIC else .Invalid
}
if value.kind == .Range {
parts := ct_child_slice(&state, value)
if len(parts) != 2 {
return nil, .Invalid
}
start, start_ok := ct_integer_value(&state, parts[0])
end, end_ok := ct_integer_value(&state, parts[1])
if !start_ok || !end_ok || end < start {
return nil, .Invalid
}
count := int(end-start)
if value.active != 0 {
count += 1
}
if count < 0 || count > COMPTIME_EVAL_QUOTA {
return nil, .Quota
}
bindings := make([]Static_Binding, count, checker.allocator)
for index in 0..<count {
id := ct_add_value(&state, Ct_Value{
kind=.Integer,
type=types.child_type(value.type, &checker.module.types),
integer=start+i128(index),
})
bindings[index] = store_static_binding(checker, &state, id, capture)
}
return bindings, .None
}
owned_children: [dynamic]Ct_Value_Id
owned_children.allocator = checker.allocator
defer delete(owned_children)
children: []Ct_Value_Id
if value.kind == .Array {
children = ct_child_slice(&state, value)
} else if value.kind == .Slice {
for index in 0..<int(value.count) {
place, _, _ := ct_slice_element_place(&state, value, index)
child, child_ok := ct_place_get(&state, place)
if !child_ok {
return nil, .Invalid
}
append(&owned_children, child)
}
children = owned_children[:]
} else if value.kind == .Struct {
item, tuple_ok := types.node(&checker.module.types, value.type)
if !tuple_ok || item.kind != .Struct || !item.tuple {
return nil, .Invalid
}
children = ct_child_slice(&state, value)
} else {
return nil, .Invalid
}
bindings := make([]Static_Binding, len(children), checker.allocator)
for child, index in children {
if child == INVALID_CT_VALUE || int(child) >= len(state.values) {
delete(bindings, checker.allocator)
return nil, .Invalid
}
bindings[index] = store_static_binding(checker, &state, child, capture)
}
return bindings, .None
}
push_expand_binding :: proc(
checker: ^Checker,
binding: Static_Binding,
index_name: symbol.Id,
index: int,
statement: ast.Stmt_Id,
) -> int {
start := len(checker.static_bindings)
append(&checker.static_bindings, binding)
append(&checker.expand_context, Expand_Expansion{statement=statement, index=u32(index)})
if symbol.is_valid(index_name) {
value := ct_add_value(&checker.static_state, Ct_Value{kind=.Integer, type=types.USIZE, integer=i128(index)})
append(&checker.static_bindings, Static_Binding{name=index_name, type=types.USIZE, value=value})
}
return start
}
pop_expand_binding :: proc(checker: ^Checker, start: int) {
resize(&checker.static_bindings, start)
_ = pop(&checker.expand_context)
}
Expand_Control :: enum u8 {
Normal,
Break,
Continue,
Invalid,
}
expand_control_target :: proc(statement: ast.Stmt, target_label: symbol.Id, allow_unlabeled: bool) -> Expand_Control {
if statement.kind != .Break && statement.kind != .Continue {
return .Normal
}
if symbol.is_valid(statement.label) {
if !symbol.is_valid(target_label) || statement.label != target_label {
return .Normal
}
} else if !allow_unlabeled {
return .Normal
}
return .Break if statement.kind == .Break else .Continue
}
contains_expand_control :: proc(
checker: ^Checker,
statements: []ast.Stmt_Id,
target_label: symbol.Id,
allow_unlabeled: bool,
) -> bool {
for statement_id in statements {
statement := checker.ast_module.statements[statement_id]
if expand_control_target(statement, target_label, allow_unlabeled) != .Normal {
return true
}
#partial switch statement.kind {
case .Block:
if contains_expand_control(checker, statement.body, target_label, allow_unlabeled) {
return true
}
case .If:
if contains_expand_control(checker, statement.body, target_label, allow_unlabeled) ||
contains_expand_control(checker, statement.else_body, target_label, allow_unlabeled) {
return true
}
case .Match, .Match_Arm:
if contains_expand_control(checker, statement.body, target_label, allow_unlabeled) {
return true
}
case .For, .While:
// Unlabelled control belongs to the nested loop. A labelled jump can still
// name the surrounding inline loop and is therefore relevant here.
if contains_expand_control(checker, statement.body, target_label, false) {
return true
}
case .Defer:
if statement.update != ast.INVALID_STMT &&
contains_expand_control(checker, []ast.Stmt_Id{statement.update}, target_label, false) {
return true
}
}
}
return false
}
copy_stmt_ids :: proc(checker: ^Checker, source_ids: []ast.Stmt_Id) -> []ast.Stmt_Id {
result := make([]ast.Stmt_Id, len(source_ids), checker.ast_module.allocator)
copy(result, source_ids)
return result
}
copy_symbols :: proc(checker: ^Checker, source_ids: []symbol.Id) -> []symbol.Id {
result := make([]symbol.Id, len(source_ids), checker.ast_module.allocator)
copy(result, source_ids)
return result
}
copy_expr_ids :: proc(checker: ^Checker, source_ids: []ast.Expr_Id) -> []ast.Expr_Id {
result := make([]ast.Expr_Id, len(source_ids), checker.ast_module.allocator)
copy(result, source_ids)
return result
}
clone_statement_body :: proc(checker: ^Checker, statement: ast.Stmt, body: []ast.Stmt_Id) -> ast.Stmt_Id {
clone := statement
clone.captures = copy_symbols(checker, statement.captures)
clone.patterns = copy_expr_ids(checker, statement.patterns)
clone.body = copy_stmt_ids(checker, body)
clone.else_body = copy_stmt_ids(checker, statement.else_body)
id := ast.stmt_id(len(checker.ast_module.statements))
append(&checker.ast_module.statements, clone)
return id
}
append_expand_block :: proc(checker: ^Checker, span: source.Span, body: []ast.Stmt_Id, out: ^[dynamic]ast.Stmt_Id) {
if len(body) == 0 {
return
}
id := ast.stmt_id(len(checker.ast_module.statements))
append(&checker.ast_module.statements, ast.Stmt{
kind=.Block,
span=span,
body=copy_stmt_ids(checker, body),
guard=ast.INVALID_EXPR,
target=ast.INVALID_EXPR,
expr=ast.INVALID_EXPR,
update=ast.INVALID_STMT,
diagnostic=source.INVALID_DIAGNOSTIC,
})
append(out, id)
}
same_stmt_ids :: proc(left, right: []ast.Stmt_Id) -> bool {
if len(left) != len(right) {
return false
}
for item, index in left {
if item != right[index] {
return false
}
}
return true
}
flatten_expand_iteration :: proc(
checker: ^Checker,
statements: []ast.Stmt_Id,
pkg: ast.Package_Id,
file: ast.File_Id,
out: ^[dynamic]ast.Stmt_Id,
target_label: symbol.Id,
diagnostic: ^source.Diagnostic_Id,
) -> Expand_Control {
for statement_id in statements {
statement := checker.ast_module.statements[statement_id]
if control := expand_control_target(statement, target_label, true); control != .Normal {
return control
}
if statement.kind == .If && len(statement.captures) == 0 {
if selected, comptime_ok := specialization_bool(checker, statement.expr, pkg, file); comptime_ok {
selected_body := statement.body if selected else statement.else_body
branch: [dynamic]ast.Stmt_Id
branch.allocator = checker.allocator
flow := flatten_expand_iteration(checker, selected_body, pkg, file, &branch, target_label, diagnostic)
append_expand_block(checker, statement.span, branch[:], out)
delete(branch)
if flow != .Normal {
return flow
}
continue
}
if contains_expand_control(checker, statement.body, target_label, true) ||
contains_expand_control(checker, statement.else_body, target_label, true) {
if diagnostic != nil {
diagnostic^ = source.add(
checker.diagnostics, statement.span,
"break or continue targeting an inline loop must be compile-time-resolvable",
)
}
return .Invalid
}
}
if statement.kind == .Match {
if selected_body, _, _, comptime_ok := specialization_match_body(checker, statement, pkg, file); comptime_ok {
selected: [dynamic]ast.Stmt_Id
selected.allocator = checker.allocator
flow := flatten_expand_iteration(checker, selected_body, pkg, file, &selected, target_label, diagnostic)
if flow != .Normal {
arm_index := -1
for arm_id, index in statement.body {
if same_stmt_ids(checker.ast_module.statements[arm_id].body, selected_body) {
arm_index = index
break
}
}
if arm_index >= 0 && len(selected) > 0 {
arm_id := statement.body[arm_index]
cloned_arm := clone_statement_body(checker, checker.ast_module.statements[arm_id], selected[:])
arms := copy_stmt_ids(checker, statement.body)
arms[arm_index] = cloned_arm
cloned_match := clone_statement_body(checker, statement, arms)
delete(arms, checker.ast_module.allocator)
append(out, cloned_match)
}
delete(selected)
return flow
}
delete(selected)
append(out, statement_id)
continue
}
if contains_expand_control(checker, statement.body, target_label, true) {
if diagnostic != nil {
diagnostic^ = source.add(
checker.diagnostics, statement.span,
"break or continue targeting an inline loop must be compile-time-resolvable",
)
}
return .Invalid
}
}
if statement.kind == .Block {
block: [dynamic]ast.Stmt_Id
block.allocator = checker.allocator
flow := flatten_expand_iteration(checker, statement.body, pkg, file, &block, target_label, diagnostic)
if flow == .Normal {
append(out, statement_id)
} else {
append_expand_block(checker, statement.span, block[:], out)
}
delete(block)
if flow != .Normal {
return flow
}
continue
}
if (statement.kind == .For || statement.kind == .While || statement.kind == .Defer) &&
contains_expand_control(checker, []ast.Stmt_Id{statement_id}, target_label, false) {
if diagnostic != nil {
diagnostic^ = source.add(
checker.diagnostics, statement.span,
"break or continue targeting an inline loop must be compile-time-resolvable",
)
}
return .Invalid
}
append(out, statement_id)
}
return .Normal
}
build_block :: proc(
ctx: ^Build_Ctx,
statements: []ast.Stmt_Id,
duplicate_scope_start := -1,
close := true,
) -> []hir.Stmt_Id {
checker := ctx.checker
store := &checker.module.types
body: [dynamic]hir.Stmt_Id
body.allocator = checker.allocator
scope_start := len(ctx.locals^)
defer_start := len(ctx.defers^)
duplicate_start := scope_start if duplicate_scope_start < 0 else duplicate_scope_start
for statement_id in statements {
statement := checker.ast_module.statements[statement_id]
if statement.diagnostic != source.INVALID_DIAGNOSTIC {
if statement.kind == .Declaration && symbol.is_valid(statement.name) {
if _, found := find_build_local(ctx.locals^[duplicate_start:], statement.name); !found {
_ = append_build_local(
ctx, statement.name, types.I64, !statement.immutable, statement.span,
)
}
}
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
continue
}
switch statement.kind {
case .Declaration:
// A value block (`x :: { ... yield v }` / `x T := { ... }`): the parser
// leaves `expr` invalid and stashes the block in `body`. Build it, then
// declare the local from the yielded value (its type for an untyped `::`).
if statement.expr == ast.INVALID_EXPR {
declared := type_from_syntax(checker, statement.type, ctx.pkg, ctx.file)
expected := declared if is_runtime_type(checker, declared) else types.INVALID
if !is_runtime_type(checker, expected) &&
int(statement_id) < len(ctx.local_types) &&
is_runtime_type(checker, ctx.local_types[statement_id]) {
expected = ctx.local_types[statement_id]
}
value, value_type := build_value_source(ctx, &body, statement.body, expected, statement.span, statement.label, statement.value_control_flow)
if _, found := find_build_local(ctx.locals^[duplicate_start:], statement.name); found {
id := source.addf(
checker.diagnostics, statement.span,
"duplicate local '%s'", symbol_text(checker, statement.name),
)
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = id,
})
ctx.problematic^ = true
continue
}
if id := add_shadow_diagnostic(
checker, statement.span, statement.name, "local",
ctx.pkg, ctx.file, ctx.locals^[:], ctx.loop_labels^[:], ctx.yield_targets^[:],
); id != source.INVALID_DIAGNOSTIC {
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = id,
})
ctx.problematic^ = true
continue
}
local_id := append_build_local(ctx, statement.name, value_type, !statement.immutable, statement.span)
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Declaration, span = statement.span, local = local_id, expr = value,
diagnostic = source.INVALID_DIAGNOSTIC,
})
continue
}
declared := resolve_inferred_array(checker, type_from_syntax(checker, statement.type, ctx.pkg, ctx.file), statement.expr)
// Adopt the type inference resolved for this local when the declaration has no
// concrete annotation and inference carried useful numeric context: constraints,
// `undefined`, open numeric constants, or arithmetic expressions.
open_const_decl := !is_runtime_type(checker, declared) && !is_undefined_expr(checker, statement.expr)
if open_const_decl {
constant := eval_integer_constant_in_context(checker, statement.expr, ctx.pkg, ctx.file)
open_const_decl = constant.kind == .Value && fits_i64(constant.value) ||
is_float_constant_expr(checker, statement.expr)
}
numeric_arithmetic_decl := !is_runtime_type(checker, declared) &&
is_numeric_arithmetic_expr(checker, statement.expr)
if statement_id != ast.INVALID_STMT && int(statement_id) < len(ctx.local_types) &&
is_runtime_type(checker, ctx.local_types[statement_id]) &&
(types.is_constraint(declared) || is_undefined_expr(checker, statement.expr) ||
open_const_decl || numeric_arithmetic_decl || has_inferred_array_count(checker, declared)) {
declared = ctx.local_types[statement_id]
}
// A still-unresolved constraint means the initializer's numeric
// family did not satisfy `int`/`float` (`undefined` reports its own).
if types.is_constraint(declared) && !is_undefined_expr(checker, statement.expr) {
id := source.addf(
checker.diagnostics,
statement.span,
"could not resolve the '%s' constraint for local '%s'",
types.name(declared),
symbol_text(checker, statement.name),
)
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = id,
})
ctx.problematic^ = true
continue
}
expected := types.INVALID
value := hir.INVALID_EXPR
value_type := types.INVALID
if is_undefined_expr(checker, statement.expr) {
if statement.immutable {
id := source.add(
checker.diagnostics,
statement.span,
"'undefined' requires a mutable local declaration",
)
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = id,
})
ctx.problematic^ = true
continue
}
if !is_runtime_type(checker, declared) || has_inferred_array_count(checker, declared) {
id := source.addf(
checker.diagnostics,
statement.span,
"could not infer a concrete type for local '%s'",
symbol_text(checker, statement.name),
)
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = id,
})
ctx.problematic^ = true
continue
}
value_type = declared
} else {
if is_runtime_type(checker, declared) {
expected = declared
}
value = build_expr(
checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls,
expected, ctx.pkg, ctx.file,
)
value_type = checker.module.exprs[value].type
if is_runtime_type(checker, declared) {
value = coerce_expr(checker, value, declared, statement.span)
value_type = declared
} 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 types.is_comptime_only(value_type, &checker.module.types) {
id := source.addf(
checker.diagnostics,
statement.span,
"local '%s' has a comptime-only type and cannot be stored at runtime",
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 types.is_noreturn(value_type) {
id := source.addf(
checker.diagnostics,
statement.span,
"local '%s' cannot store a noreturn value",
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,
})
if _, found := find_build_local(ctx.locals^[duplicate_start:], statement.name); !found {
_ = append_build_local(ctx, statement.name, types.I64, !statement.immutable, statement.span)
}
ctx.problematic^ = true
continue
}
if _, found := find_build_local(ctx.locals^[duplicate_start:], statement.name); found {
id := source.addf(
checker.diagnostics, statement.span,
"duplicate local '%s'", symbol_text(checker, statement.name),
)
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = id,
})
ctx.problematic^ = true
continue
}
if id := add_shadow_diagnostic(
checker, statement.span, statement.name, "local",
ctx.pkg, ctx.file, ctx.locals^[:], ctx.loop_labels^[:], ctx.yield_targets^[:],
); id != source.INVALID_DIAGNOSTIC {
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = id,
})
ctx.problematic^ = true
continue
}
local_id := append_build_local(ctx, statement.name, value_type, !statement.immutable, statement.span)
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Declaration, span = statement.span, local = local_id, expr = value,
diagnostic = source.INVALID_DIAGNOSTIC,
})
if value != hir.INVALID_EXPR {
ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid
}
case .Assignment:
if statement.target != ast.INVALID_EXPR && !symbol.is_valid(statement.name) {
target_expr := build_expr(
checker, statement.target, ctx.locals^[:], ctx.global_reads, ctx.calls,
types.INVALID, ctx.pkg, ctx.file,
)
if diagnostic, invalid := invalid_expr_diagnostic(checker, target_expr); invalid {
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=diagnostic,
})
ctx.problematic^ = true
continue
}
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
} else if statement.assignment_op == .Shift_Left ||
statement.assignment_op == .Shift_Right ||
statement.assignment_op == .Shift_Left_Saturating {
rhs_expected = types.U64
}
rhs_numeric_operation := false
if statement.assignment_op != .Shift_Left &&
statement.assignment_op != .Shift_Right &&
statement.assignment_op != .Shift_Left_Saturating &&
is_numeric_constant_expr(checker, statement.expr) {
_, rhs_numeric_operation = types.distinct_scalar_backing(target_type, &checker.module.types)
}
value = build_expr(
checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls,
rhs_expected, ctx.pkg, ctx.file, rhs_numeric_operation,
)
if diagnostic, invalid := invalid_expr_diagnostic(checker, value); invalid {
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=diagnostic,
})
ctx.problematic^ = true
continue
}
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
case .Bit_And: assignment_op = .Bit_And
case .Bit_Or: assignment_op = .Bit_Or
case .Bit_Xor: assignment_op = .Bit_Xor
case .Shift_Left: assignment_op = .Shift_Left
case .Shift_Right: assignment_op = .Shift_Right
case .Shift_Left_Saturating: assignment_op = .Shift_Left_Saturating
}
rhs_type := checker.module.exprs[value].type
is_shift := statement.assignment_op == .Shift_Left ||
statement.assignment_op == .Shift_Right ||
statement.assignment_op == .Shift_Left_Saturating
is_bitwise := statement.assignment_op == .Bit_And ||
statement.assignment_op == .Bit_Or ||
statement.assignment_op == .Bit_Xor
operation, compatible := numeric_operation_type(checker, target_type, rhs_type)
target_representation := types.runtime_representation(target_type, &checker.module.types)
if is_shift {
if !types.is_concrete_integer(target_representation) || !types.is_unsigned(rhs_type, checker.target) {
id := source.add(checker.diagnostics, statement.span, "shift assignment requires an integer target and unsigned integer count")
value = invalid_hir_expr(checker, statement.span, id, target_type)
} else if constant := eval_integer_constant_in_context(checker, statement.expr, ctx.pkg, ctx.file);
constant.kind == .Value && statement.assignment_op != .Shift_Left_Saturating &&
constant.value >= i128(types.bits(target_representation, checker.target)) {
id := source.addf(
checker.diagnostics, statement.span,
"shift count %d exceeds %s width", constant.value, types.name(target_type),
)
value = invalid_hir_expr(checker, statement.span, id, target_type)
}
} else if is_bitwise {
if !compatible || !types.is_concrete_integer(operation.representation) {
id := source.add(checker.diagnostics, statement.span, "bitwise assignment requires compatible concrete integer operands")
value = invalid_hir_expr(checker, statement.span, id, target_type)
} else {
value = coerce_expr(checker, value, target_type, statement.span)
}
} else if statement.assignment_op == .Div &&
compatible && types.is_concrete_integer(operation.representation) {
id := source.add(
checker.diagnostics,
statement.span,
"integer '/=' is not allowed; assign through an explicit division builtin",
)
value = invalid_hir_expr(checker, statement.span, id, target_type)
} else if !compatible || types.is_bool(operation.representation) {
id := source.add(
checker.diagnostics,
statement.span,
"arithmetic requires compatible numeric operands",
)
value = invalid_hir_expr(checker, statement.span, id, target_type)
} else {
// Compound assignment stores back into the original
// target type, so only an equal or widening RHS
// conversion is permitted.
value = coerce_expr(checker, value, target_type, statement.span)
}
}
} else if statement.expr == ast.INVALID_EXPR {
// `target = { ... yield v }`: build the value block against the
// target's type (build_value_block coerces internally).
value, _ = build_value_source(ctx, &body, statement.body, target_type, statement.span, statement.label, statement.value_control_flow)
} else {
value = build_expr(
checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls,
target_type, ctx.pkg, ctx.file,
)
value = coerce_expr(checker, value, target_type, statement.span)
}
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind=.Assignment, span=statement.span, local=hir.INVALID_LOCAL,
assignment_op=assignment_op,
target=target_expr, expr=value, diagnostic=source.INVALID_DIAGNOSTIC,
})
ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid
continue
}
if statement.name == checker.sink_symbol {
value: hir.Expr_Id
if statement.expr == ast.INVALID_EXPR {
value, _ = build_value_source(ctx, &body, statement.body, types.INVALID, statement.span, statement.label, statement.value_control_flow)
} else {
value = build_expr(checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, types.INVALID, ctx.pkg, ctx.file)
}
if types.is_void(checker.module.exprs[value].type) {
id := source.add(checker.diagnostics, statement.span, "cannot assign a void expression to '_'")
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = id,
})
ctx.problematic^ = true
} else {
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Sink, span = statement.span, expr = value,
local = hir.INVALID_LOCAL, diagnostic = source.INVALID_DIAGNOSTIC,
})
}
continue
}
if _, is_comptime := current_comptime_value(checker, statement.name); is_comptime {
id := source.addf(
checker.diagnostics,
statement.span,
"cannot assign comptime parameter '%s'",
symbol_text(checker, statement.name),
)
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = id,
})
ctx.problematic^ = true
continue
}
local, found := find_build_local(ctx.locals^[:], statement.name)
if !found {
global := find_global(checker, statement.name, ctx.pkg, ctx.file)
if global == ast.INVALID_GLOBAL {
id := source.addf(checker.diagnostics, statement.span, "cannot assign unresolved local '%s'", symbol_text(checker, statement.name))
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = id,
})
ctx.problematic^ = true
continue
}
target_expr := build_global_reference(checker, global, statement.span, ctx.global_reads)
if diagnostic, invalid := invalid_expr_diagnostic(checker, target_expr); invalid {
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=diagnostic,
})
ctx.problematic^ = true
continue
}
target_type := checker.module.exprs[target_expr].type
if !hir_location_writable(checker, target_expr, ctx.locals^[:]) {
id := source.add(checker.diagnostics, statement.span, "assignment target is not writable")
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind=.Trap, span=statement.span, local=hir.INVALID_LOCAL,
target=hir.INVALID_EXPR, expr=hir.INVALID_EXPR, diagnostic=id,
})
ctx.problematic^ = true
continue
}
value: hir.Expr_Id
if statement.expr == ast.INVALID_EXPR {
value, _ = build_value_source(ctx, &body, statement.body, target_type, statement.span, statement.label, statement.value_control_flow)
} else {
value = build_expr(
checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls,
target_type, ctx.pkg, ctx.file,
)
value = coerce_expr(checker, value, target_type, statement.span)
}
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind=.Assignment, span=statement.span, local=hir.INVALID_LOCAL,
assignment_op=.Set,
target=target_expr, expr=value, diagnostic=source.INVALID_DIAGNOSTIC,
})
ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid
continue
}
if !local.mutable {
id := source.addf(checker.diagnostics, statement.span, "cannot assign immutable local '%s'", symbol_text(checker, statement.name))
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = id,
})
ctx.problematic^ = true
continue
}
value: hir.Expr_Id
if statement.expr == ast.INVALID_EXPR {
value, _ = build_value_source(ctx, &body, statement.body, local.type, statement.span, statement.label, statement.value_control_flow)
} else {
value = build_expr(
checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls,
local.type, ctx.pkg, ctx.file,
)
value = coerce_expr(checker, value, local.type, statement.span)
}
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Assignment, span = statement.span, expr = value, local = local.id,
target = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC,
})
ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid
case .Return:
if ctx.defer_depth > 0 {
id := source.add(checker.diagnostics, statement.span, "cannot 'return' inside a 'defer'")
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = id,
})
ctx.problematic^ = true
continue
}
if statement.expr == ast.INVALID_EXPR && !statement.value_control_flow {
if types.kind(ctx.result, store) == .Fallible &&
types.is_void(types.fallible_success(ctx.result, store)) {
flush_defers(ctx, &body, 0)
value := fallible_aggregate(checker, statement.span, ctx.result, hir.INVALID_EXPR, false)
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Return, span = statement.span, expr = value,
local = hir.INVALID_LOCAL, diagnostic = source.INVALID_DIAGNOSTIC,
})
} else if !types.is_void(ctx.result) {
id := source.add(checker.diagnostics, statement.span, "non-void function must 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
} else {
flush_defers(ctx, &body, 0)
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Return, span = statement.span, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = source.INVALID_DIAGNOSTIC,
})
}
continue
}
if types.is_void(ctx.result) {
id := source.add(checker.diagnostics, statement.span, "void function cannot return a value")
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = id,
})
ctx.problematic^ = true
continue
}
value := hir.INVALID_EXPR
error_exit := false
error_value := hir.INVALID_EXPR
if statement.value_control_flow {
if types.kind(ctx.result, store) == .Fallible {
success := types.fallible_success(ctx.result, store)
value, _ = build_value_source(ctx, &body, statement.body, success, statement.span, symbol.INVALID, statement.value_control_flow)
value = coerce_expr(checker, value, success, statement.span)
value = fallible_aggregate(checker, statement.span, ctx.result, value, false)
} else {
value, _ = build_value_source(ctx, &body, statement.body, ctx.result, statement.span, symbol.INVALID, statement.value_control_flow)
value = coerce_expr(checker, value, ctx.result, statement.span)
}
} else if types.kind(ctx.result, store) == .Fallible {
success := types.fallible_success(ctx.result, store)
error_type := types.fallible_error(ctx.result, store)
expr_ast := checker.ast_module.exprs[statement.expr]
if expr_ast.kind == .Enum_Literal {
success_has := types.sum_has_name(store, success, u32(expr_ast.name))
error_has := types.sum_has_name(store, error_type, u32(expr_ast.name))
if error_has && !success_has {
error_exit = true
} else if error_has && success_has {
id := source.add(checker.diagnostics, expr_ast.span, "ambiguous fallible return member")
value = invalid_hir_expr(checker, expr_ast.span, id, ctx.result)
}
} else if expr_ast.kind == .Struct_Literal {
target_pkg, available := expr_package(checker, expr_ast, ctx.pkg, ctx.file, true)
named := types.find_named(store, u32(target_pkg), u32(expr_ast.name), file=u32(expr_lookup_file(expr_ast, ctx.file))) if available else types.INVALID
named = types.resolve_alias(named, store)
error_exit = can_implicitly_convert_type(checker, named, error_type)
} else {
probe := build_expr(
checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls,
types.INVALID, ctx.pkg, ctx.file,
)
probe_type := checker.module.exprs[probe].type
returns_success := can_implicitly_convert_type(checker, probe_type, success)
returns_error := can_implicitly_convert_type(checker, probe_type, error_type)
if returns_error && !returns_success {
error_exit = true
value = probe
} else if !returns_success {
if projected, refined := refined_error_projection(ctx, probe, error_type, statement.span); refined {
error_exit = true
value = projected
} else if types.is_enum(probe_type, store) || types.is_tagged_union(probe_type, store) {
id := source.addf(
checker.diagnostics, statement.span,
"cannot return %s as success type %s or error type %s",
type_label(checker, probe_type), type_label(checker, success), type_label(checker, error_type),
)
value = invalid_hir_expr(checker, statement.span, id, success)
}
} else {
value = probe
}
}
if value == hir.INVALID_EXPR {
expected := error_type if error_exit else success
value = build_expr(
checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls,
expected, ctx.pkg, ctx.file,
)
}
expected := error_type if error_exit else success
value = coerce_expr(checker, value, expected, statement.span)
if error_exit && len(ctx.defers^) > 0 && checker.module.exprs[value].kind != .Invalid {
tmp := append_tracked_local(
ctx.hir_locals, ctx.local_spans, ctx.local_used, ctx.local_warnable,
hir.Local{name=checker.sink_symbol, type=error_type, mutable=false}, source.Span{},
)
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind=.Declaration, span=statement.span, local=tmp, expr=value,
diagnostic=source.INVALID_DIAGNOSTIC,
})
error_value = hir.expr_id(len(checker.module.exprs))
append(&checker.module.exprs, hir.Expr{
kind=.Local, span=statement.span, type=error_type, target=hir.local_ref(tmp),
})
value = error_value
}
value = fallible_aggregate(checker, statement.span, ctx.result, value, error_exit)
} else {
value = build_expr(
checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls,
ctx.result, ctx.pkg, ctx.file,
)
value = coerce_expr(checker, value, ctx.result, statement.span)
}
ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid
// Run deferred statements before returning, but capture the return value
// first (spill it to a temp) so a defer that mutates the returned local
// can't change what is returned — Zig evaluates the return value, then
// runs defers.
if len(ctx.defers^) > 0 {
if !error_exit && checker.module.exprs[value].kind != .Invalid {
tmp := append_tracked_local(
ctx.hir_locals,
ctx.local_spans,
ctx.local_used,
ctx.local_warnable,
hir.Local{name = checker.sink_symbol, type = ctx.result, mutable = false},
source.Span{},
)
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Declaration, span = statement.span, local = tmp, expr = value,
diagnostic = source.INVALID_DIAGNOSTIC,
})
value = hir.expr_id(len(checker.module.exprs))
append(&checker.module.exprs, hir.Expr{
kind = .Local, span = statement.span, type = ctx.result, target = hir.local_ref(tmp),
})
}
flush_defers(ctx, &body, 0, error_exit, error_value)
}
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Return, span = statement.span, expr = value,
local = hir.INVALID_LOCAL, diagnostic = source.INVALID_DIAGNOSTIC,
})
case .Expression:
value := build_expr(checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, types.INVALID, ctx.pkg, ctx.file)
if diagnostic, invalid := invalid_expr_diagnostic(checker, value); 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 = diagnostic,
})
ctx.problematic^ = true
} else if !types.is_void(checker.module.exprs[value].type) &&
!types.is_noreturn(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 {
if selected, comptime_ok := specialization_bool(checker, statement.expr, ctx.pkg, ctx.file); comptime_ok {
selected_body := statement.body if selected else statement.else_body
branch := build_block(ctx, selected_body)
append(&body, ..branch)
delete(branch, checker.allocator)
continue
}
}
if len(statement.captures) > 0 {
ast_operands: [dynamic]ast.Expr_Id
ast_operands.allocator = checker.allocator
flatten_conditional_unwrap_operands(checker.ast_module, statement.expr, &ast_operands)
valid_unwrap := true
diagnostic := source.INVALID_DIAGNOSTIC
if len(ast_operands) != len(statement.captures) {
diagnostic = source.addf(
checker.diagnostics,
statement.span,
"'if' unwrap has %d operands but %d captures",
len(ast_operands),
len(statement.captures),
)
valid_unwrap = false
}
values := make([]hir.Expr_Id, len(ast_operands), checker.allocator)
child_types := make([]types.Type, len(ast_operands), checker.allocator)
for operand, index in ast_operands {
child_types[index] = types.INVALID
value := build_expr(
checker, operand, ctx.locals^[:], ctx.global_reads, ctx.calls,
types.INVALID, ctx.pkg, ctx.file,
)
values[index] = value
value_type := checker.module.exprs[value].type
if checker.module.exprs[value].kind == .Invalid {
valid_unwrap = false
if diagnostic == source.INVALID_DIAGNOSTIC {
diagnostic = checker.module.exprs[value].diagnostic
}
} else if !types.is_optional(value_type, &checker.module.types) {
diagnostic = source.addf(
checker.diagnostics,
checker.ast_module.exprs[operand].span,
"'if' unwrap requires an optional value (operand %d)",
index + 1,
)
valid_unwrap = false
} else {
child_types[index] = types.child_type(value_type, &checker.module.types)
}
}
capture_start := len(ctx.locals^)
unwraps: [dynamic]hir.Conditional_Unwrap
unwraps.allocator = checker.allocator
for capture, index in statement.captures {
child := child_types[index] if index < len(child_types) else types.INVALID
local := hir.INVALID_LOCAL
if capture != checker.sink_symbol {
if _, duplicate := find_build_local(ctx.locals^[capture_start:], capture); duplicate {
diagnostic = source.add(
checker.diagnostics,
statement.span,
"'if' unwrap captures must have distinct names",
)
valid_unwrap = false
} else if id := add_shadow_diagnostic(
checker, statement.span, capture, "capture",
ctx.pkg, ctx.file, ctx.locals^[:capture_start], ctx.loop_labels^[:], ctx.yield_targets^[:],
); id != source.INVALID_DIAGNOSTIC {
diagnostic = id
valid_unwrap = false
}
local = append_build_local(ctx, capture, child, false, statement.span)
}
if index < len(values) {
append(&unwraps, hir.Conditional_Unwrap{expr=values[index], local=local})
}
}
guard := hir.INVALID_EXPR
if statement.guard != ast.INVALID_EXPR {
guard = build_expr(
checker, statement.guard, ctx.locals^[:], ctx.global_reads, ctx.calls,
types.BOOL, ctx.pkg, ctx.file,
)
if checker.module.exprs[guard].kind == .Invalid {
valid_unwrap = false
if diagnostic == source.INVALID_DIAGNOSTIC {
diagnostic = checker.module.exprs[guard].diagnostic
}
} else if !types.is_bool(checker.module.exprs[guard].type) &&
!types.is_noreturn(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) &&
!types.is_noreturn(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) &&
!types.is_noreturn(checker.module.exprs[condition].type) {
id := source.add(checker.diagnostics, statement.span, "'while' condition must be a bool")
condition = invalid_hir_expr(checker, statement.span, id, types.BOOL)
ctx.problematic^ = true
}
if id := add_label_shadow_diagnostic(ctx, statement.span, statement.label);
id != source.INVALID_DIAGNOSTIC {
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = id,
})
ctx.problematic^ = true
}
append(ctx.loop_defer_starts, len(ctx.defers^))
append(ctx.loop_labels, statement.label)
append(ctx.loop_is_loop, true)
loop_body := build_block(ctx, statement.body)
pop(ctx.loop_is_loop)
pop(ctx.loop_labels)
pop(ctx.loop_defer_starts)
update := hir.INVALID_STMT
if statement.update != ast.INVALID_STMT {
update_ast := [1]ast.Stmt_Id{statement.update}
update_body := build_block(ctx, update_ast[:])
if len(update_body) > 0 {
update = update_body[0]
}
delete(update_body, checker.allocator)
}
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind=.While,
span=statement.span,
label=statement.label,
expr=condition,
then_body=loop_body,
update=update,
local=hir.INVALID_LOCAL,
target=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
ctx.problematic^ = ctx.problematic^ || checker.module.exprs[condition].kind == .Invalid
case .For:
if statement.expand {
if statement.pointer_capture {
id := source.add(checker.diagnostics, statement.span, "inline for does not support pointer captures")
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{kind=.Trap, span=statement.span, diagnostic=id})
ctx.problematic^ = true
continue
}
bindings, expand_error := expand_field_bindings(checker, statement.expr, statement.name, ctx.pkg, ctx.file, true)
if expand_error != .None && expand_error != .Diagnosed {
message := "inline for requires a comptime tuple, fixed array, range, slice, or reflection value"
if expand_error == .Quota {
message = "inline for expansion exceeds the compile-time evaluation quota"
}
id := source.add(
checker.diagnostics, statement.span,
message,
)
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{kind=.Trap, span=statement.span, diagnostic=id})
ctx.problematic^ = true
} else if expand_error == .None {
for binding, expand_index in bindings {
binding_start := push_expand_binding(checker, binding, statement.index_name, expand_index, statement_id)
iteration: [dynamic]ast.Stmt_Id
iteration.allocator = checker.allocator
diagnostic := source.INVALID_DIAGNOSTIC
control := flatten_expand_iteration(
checker, statement.body, ctx.pkg, ctx.file, &iteration, statement.label, &diagnostic,
)
if control == .Invalid {
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind=.Trap, span=statement.span, diagnostic=diagnostic,
})
ctx.problematic^ = true
} else {
expanded := build_block(ctx, iteration[:])
append(&body, ..expanded)
delete(expanded, checker.allocator)
}
delete(iteration)
pop_expand_binding(checker, binding_start)
if control == .Break || control == .Invalid {
break
}
}
}
delete(bindings, checker.allocator)
continue
}
iterable := build_expr(
checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls,
types.INVALID, ctx.pkg, ctx.file,
)
iterable_type := checker.module.exprs[iterable].type
capture_type := types.I64
iterator_type := types.INVALID
valid_loop := checker.module.exprs[iterable].kind != .Invalid
diagnostic := source.INVALID_DIAGNOSTIC
is_range := types.is_range(iterable_type, &checker.module.types)
if is_range {
capture_type = types.child_type(iterable_type, &checker.module.types)
if statement.pointer_capture {
diagnostic = source.add(checker.diagnostics, statement.span, "range loops do not support pointer captures")
valid_loop = false
}
if symbol.is_valid(statement.index_name) {
diagnostic = source.add(checker.diagnostics, statement.span, "range loops do not support index captures")
valid_loop = false
}
} else {
item, ok := sequence_item(iterable_type, &checker.module.types)
if !ok {
diagnostic = source.add(
checker.diagnostics,
statement.span,
"for-loop iterable must be a range, array, slice, or pointer-to-array",
)
valid_loop = false
} else {
iterator_type = types.pointer(
&checker.module.types,
item.child,
item.mutable,
true,
item.has_sentinel,
item.sentinel,
)
capture_type = item.child
if statement.pointer_capture {
_, _, array_pointer_ok := types.array_pointer(iterable_type, &checker.module.types)
if !types.is_slice(iterable_type, &checker.module.types) && !array_pointer_ok {
diagnostic = source.add(
checker.diagnostics,
statement.span,
"pointer capture over an array requires a pointer-to-array such as '&items'",
)
valid_loop = false
}
capture_type = types.pointer(&checker.module.types, item.child, item.mutable, false)
}
}
}
capture_start := len(ctx.locals^)
if statement.name != checker.sink_symbol {
if id := add_shadow_diagnostic(
checker, statement.span, statement.name, "capture",
ctx.pkg, ctx.file, ctx.locals^[:], ctx.loop_labels^[:], ctx.yield_targets^[:],
); id != source.INVALID_DIAGNOSTIC {
diagnostic = id
valid_loop = false
}
}
item_local := append_build_local(ctx, statement.name, capture_type, false, statement.span)
index_local := hir.INVALID_LOCAL
if symbol.is_valid(statement.index_name) {
if statement.index_name == statement.name && statement.name != checker.sink_symbol {
diagnostic = source.add(checker.diagnostics, statement.span, "for-loop captures must have distinct names")
valid_loop = false
} else if id := add_shadow_diagnostic(
checker, statement.span, statement.index_name, "capture",
ctx.pkg, ctx.file, ctx.locals^[:capture_start], ctx.loop_labels^[:], ctx.yield_targets^[:],
); id != source.INVALID_DIAGNOSTIC {
diagnostic = id
valid_loop = false
} else {
index_local = append_build_local(ctx, statement.index_name, types.USIZE, false, statement.span)
}
}
if id := add_label_shadow_diagnostic(ctx, statement.span, statement.label);
id != source.INVALID_DIAGNOSTIC {
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = id,
})
ctx.problematic^ = true
}
append(ctx.loop_defer_starts, len(ctx.defers^))
append(ctx.loop_labels, statement.label)
append(ctx.loop_is_loop, true)
loop_body := build_block(ctx, statement.body, capture_start)
pop(ctx.loop_is_loop)
pop(ctx.loop_labels)
pop(ctx.loop_defer_starts)
resize(ctx.locals, capture_start)
append(&body, hir.stmt_id(len(checker.module.statements)))
if valid_loop {
append(&checker.module.statements, hir.Stmt{
kind=.For,
span=statement.span,
label=statement.label,
local=item_local,
index_local=index_local,
expr=iterable,
iterator_type=iterator_type,
pointer_capture=statement.pointer_capture,
then_body=loop_body,
update=hir.INVALID_STMT,
target=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
} else {
delete(loop_body, checker.allocator)
append(&checker.module.statements, hir.Stmt{
kind=.Trap,
span=statement.span,
local=hir.INVALID_LOCAL,
index_local=hir.INVALID_LOCAL,
expr=hir.INVALID_EXPR,
target=hir.INVALID_EXPR,
diagnostic=diagnostic,
})
ctx.problematic^ = true
}
case .Break, .Continue:
// `break :L` / `continue :L` targets the innermost enclosing loop labeled `L`;
// an unlabeled one targets the innermost loop. Inside a `defer`, `loop_floor`
// hides the loops opened outside the defer.
target_index := -1
if symbol.is_valid(statement.label) {
// `break :L` targets a labeled loop or block; `continue :L` only a loop.
for i := len(ctx.loop_labels^) - 1; i >= ctx.loop_floor; i -= 1 {
if ctx.loop_labels^[i] == statement.label &&
(ctx.loop_is_loop^[i] || statement.kind == .Break) {
target_index = i
break
}
}
} else {
// Unlabeled `break`/`continue` targets the innermost loop, skipping blocks.
for i := len(ctx.loop_defer_starts^) - 1; i >= ctx.loop_floor; i -= 1 {
if ctx.loop_is_loop^[i] {
target_index = i
break
}
}
}
if target_index < 0 {
keyword := "break" if statement.kind == .Break else "continue"
id: source.Diagnostic_Id
if symbol.is_valid(statement.label) {
id = source.addf(checker.diagnostics, statement.span,
"no enclosing loop is labeled '%s'", symbol_text(checker, statement.label))
} else {
id = source.addf(checker.diagnostics, statement.span, "'%s' outside of a loop", keyword)
}
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = id,
})
ctx.problematic^ = true
continue
}
// Exit the loop body and any blocks between here and the target loop: run their
// deferred statements down to and including the target loop body.
flush_defers(ctx, &body, ctx.loop_defer_starts^[target_index])
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Break if statement.kind == .Break else .Continue,
span = statement.span, label = statement.label, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = source.INVALID_DIAGNOSTIC,
})
case .Block:
if symbol.is_valid(statement.label) {
// A labeled block statement (`blk: { … break :blk … }`): a break target
// with an exit-label boundary, built as a HIR `.Block`. Not a loop, so
// unlabeled `break`/`continue` and `continue :blk` skip it.
if id := add_label_shadow_diagnostic(ctx, statement.span, statement.label);
id != source.INVALID_DIAGNOSTIC {
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = id,
})
ctx.problematic^ = true
}
append(ctx.loop_defer_starts, len(ctx.defers^))
append(ctx.loop_labels, statement.label)
append(ctx.loop_is_loop, false)
built := build_block(ctx, statement.body)
pop(ctx.loop_is_loop)
pop(ctx.loop_labels)
pop(ctx.loop_defer_starts)
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Block, span = statement.span, label = statement.label,
then_body = built, local = hir.INVALID_LOCAL, target = hir.INVALID_EXPR,
diagnostic = source.INVALID_DIAGNOSTIC,
})
continue
}
// A bare `{ ... }` scope: build it (its own locals/defers are scoped by
// the recursive call) and splice its statements in.
block := build_block(ctx, statement.body)
for stmt in block {
append(&body, stmt)
}
delete(block, checker.allocator)
case .Yield:
// A labeled `yield :blk x` exits the value-loop labeled `blk`: assign the
// result slot, then `break` (which flushes defers down to the loop body and
// branches to its exit). HIR holds no `.Yield` — it becomes Assignment + Break.
if symbol.is_valid(statement.label) {
target_index := -1
for i := len(ctx.yield_targets^) - 1; i >= 0; i -= 1 {
if ctx.yield_targets^[i].label == statement.label {
target_index = i
break
}
}
if target_index < 0 {
id := source.addf(checker.diagnostics, statement.span,
"no enclosing value loop or block is labeled '%s'", symbol_text(checker, statement.label))
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = id,
})
ctx.problematic^ = true
continue
}
target := &ctx.yield_targets^[target_index]
yielded := hir.INVALID_EXPR
if statement.value_control_flow {
yielded, _ = build_value_source(ctx, &body, statement.body, target.slot_type, statement.span, symbol.INVALID, statement.value_control_flow)
} else {
yielded = build_expr(checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, target.slot_type, ctx.pkg, ctx.file)
}
if yielded != hir.INVALID_EXPR && types.is_void(checker.module.exprs[yielded].type) {
id := source.add(checker.diagnostics, statement.span, "'yield' expression must produce a non-void 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
}
yielded = resolve_loop_slot(ctx, target, yielded, checker.module.exprs[yielded].type if yielded != hir.INVALID_EXPR else types.INVALID, statement.span)
if target.slot == hir.INVALID_LOCAL || yielded == hir.INVALID_EXPR {
id := source.add(checker.diagnostics, statement.span,
"could not determine the value loop's yield type; annotate the binding or yield a concrete value first")
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = id,
})
ctx.problematic^ = true
continue
}
// slot = value (the slot is un-nameable, so no defer can mutate it; no spill).
emit_slot_assign(checker, &body, target.slot, yielded, statement.span)
// Exit the target: flush defers down to its body, then a labeled break.
flush_defers(ctx, &body, target.defer_floor)
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Break, span = statement.span, label = target.label, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = source.INVALID_DIAGNOSTIC,
})
continue
}
// An unlabeled yield reaching here is misplaced: a legitimate trailing yield
// is peeled by the value builders (value block / if branch / loop fall-through).
id := source.add(
checker.diagnostics, statement.span,
"'yield' is only valid as the final statement of a value block, or as 'yield :label' inside a labeled value loop or block",
)
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = id,
})
ctx.problematic^ = true
case .Defer:
deferred := checker.ast_module.statements[statement.update]
if deferred.kind == .Return || deferred.kind == .Break ||
deferred.kind == .Continue || deferred.kind == .Defer {
keyword := "return"
if deferred.kind == .Break { keyword = "break" }
if deferred.kind == .Continue { keyword = "continue" }
if deferred.kind == .Defer { keyword = "defer" }
id := source.addf(checker.diagnostics, statement.span, "cannot defer a '%s' statement", keyword)
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = id,
})
ctx.problematic^ = true
continue
}
if statement.error_only && types.kind(ctx.result, store) != .Fallible {
id := source.add(checker.diagnostics, statement.span, "'errdefer' requires an enclosing fallible 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
continue
}
// Build the deferred statement once, guarded so a `return` inside it is
// rejected and `break`/`continue` only target loops opened within the
// defer; its hir is replayed at each scope exit, not emitted here.
capture_start := len(ctx.locals^)
capture := hir.INVALID_LOCAL
if statement.error_only && len(statement.captures) > 0 &&
statement.captures[0] != checker.sink_symbol {
capture = append_build_local(
ctx, statement.captures[0], types.fallible_error(ctx.result, store), false, statement.span,
)
}
saved_floor := ctx.loop_floor
ctx.defer_depth += 1
ctx.loop_floor = len(ctx.loop_defer_starts^)
entry := build_block(ctx, []ast.Stmt_Id{statement.update})
ctx.loop_floor = saved_floor
ctx.defer_depth -= 1
resize(ctx.locals, capture_start)
append(ctx.defers, Defer_Entry{
body=entry, error_only=statement.error_only, capture=capture,
})
case .Match:
if selected_body, capture, has_capture, comptime_ok := specialization_match_body(checker, statement, ctx.pkg, ctx.file); comptime_ok {
if has_capture {
append(&checker.static_bindings, capture)
}
expanded := build_block(ctx, selected_body)
if has_capture {
_ = pop(&checker.static_bindings)
}
append(&body, ..expanded)
delete(expanded, checker.allocator)
} else {
build_match(ctx, &body, statement)
}
case .Match_Arm:
// Arms are only reachable through their enclosing `.Match`; one on its own
// is a parser bug.
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL,
diagnostic = source.add(checker.diagnostics, statement.span, "unexpected match arm outside 'match'"),
})
ctx.problematic^ = true
case .Invalid:
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = statement.diagnostic,
})
ctx.problematic^ = true
}
}
// Normal fall-through exit: run this block's own deferred statements, unless
// every path already exited early (return/break/continue) — that would only
// emit unreachable duplicates. A value block (`close=false`) skips this so its
// caller can capture the yielded value before flushing the block's defers.
if close {
if !all_paths_exit(&checker.module, body[:]) {
flush_defers(ctx, &body, defer_start)
}
// Free this block's deferred-statement entry slices (their stmt ids were
// already replayed at every path that can leave this block) and pop the frame.
for i := defer_start; i < len(ctx.defers^); i += 1 {
delete(ctx.defers^[i].body, checker.allocator)
}
resize(ctx.defers, defer_start)
resize(ctx.locals, scope_start)
}
return body[:]
}
// build_value_block builds a `{ ... yield v }` value block whose final statement
// must be a `yield`. Catch handlers may instead exit on every path or complete with
// void, in which case `allow_exit` leaves the fallback expression invalid. Otherwise
// it builds the leading statements inline, evaluates the yield in their scope, then
// captures the value before running defers. `expected` is the binding's type (INVALID
// for an untyped `::`).
build_value_block :: proc(
ctx: ^Build_Ctx,
body: ^[dynamic]hir.Stmt_Id,
body_stmts: []ast.Stmt_Id,
expected: types.Type,
span: source.Span,
allow_exit := false,
) -> (value: hir.Expr_Id, value_type: types.Type) {
checker := ctx.checker
n := len(body_stmts)
if n == 0 ||
checker.ast_module.statements[body_stmts[n - 1]].kind != .Yield ||
symbol.is_valid(checker.ast_module.statements[body_stmts[n - 1]].label) {
// Build whatever is there so inner errors (and misplaced yields) surface, then
// report the missing trailing yield.
inner := build_block(ctx, body_stmts)
for s in inner {
append(body, s)
}
if allow_exit && (types.is_void(expected) || all_paths_exit(&checker.module, inner)) {
delete(inner, checker.allocator)
return hir.INVALID_EXPR, expected
}
delete(inner, checker.allocator)
id := source.add(checker.diagnostics, span, "a value block must end with an explicit 'yield'")
ctx.problematic^ = true
return invalid_hir_expr(checker, span, id), types.INVALID
}
scope_start := len(ctx.locals^)
defer_start := len(ctx.defers^)
// Leading statements keep the scope open (close=false) so the yield can still see
// the block's locals; any nested `yield` hits the erroring `.Yield` switch case.
leading := build_block(ctx, body_stmts[:n - 1], close = false)
for s in leading {
append(body, s)
}
delete(leading, checker.allocator)
yield_stmt := checker.ast_module.statements[body_stmts[n - 1]]
if yield_stmt.value_control_flow {
value, value_type = build_value_source(ctx, body, yield_stmt.body, expected, yield_stmt.span, symbol.INVALID, yield_stmt.value_control_flow)
} else {
value = build_expr(
checker, yield_stmt.expr, ctx.locals^[:], ctx.global_reads, ctx.calls,
expected, ctx.pkg, ctx.file,
)
value_type = checker.module.exprs[value].type
}
if types.is_void(value_type) {
id := source.add(checker.diagnostics, yield_stmt.span, "'yield' expression must produce a non-void value")
value = invalid_hir_expr(checker, yield_stmt.span, id)
value_type = types.INVALID
ctx.problematic^ = true
} else if types.is_void(expected) {
id := source.add(checker.diagnostics, yield_stmt.span, "void value context must fall through instead of yielding")
value = invalid_hir_expr(checker, yield_stmt.span, id)
value_type = types.INVALID
ctx.problematic^ = true
} else if is_runtime_type(checker, expected) {
value = coerce_expr(checker, value, expected, yield_stmt.span)
value_type = checker.module.exprs[value].type
}
ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid
// Run the block's deferred statements before the value escapes, but capture the
// value first (spill to a temp) so a defer can't change what is yielded — the same
// rule as `return`.
if len(ctx.defers^) > defer_start {
if checker.module.exprs[value].kind != .Invalid {
tmp := append_tracked_local(
ctx.hir_locals,
ctx.local_spans,
ctx.local_used,
ctx.local_warnable,
hir.Local{name = checker.sink_symbol, type = value_type, mutable = false},
source.Span{},
)
append(body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Declaration, span = yield_stmt.span, local = tmp, expr = value,
diagnostic = source.INVALID_DIAGNOSTIC,
})
value = hir.expr_id(len(checker.module.exprs))
append(&checker.module.exprs, hir.Expr{
kind = .Local, span = yield_stmt.span, type = value_type, target = hir.local_ref(tmp),
})
}
flush_defers(ctx, body, defer_start)
}
// Close the scope (build_block left it open for us).
for i := defer_start; i < len(ctx.defers^); i += 1 {
delete(ctx.defers^[i].body, checker.allocator)
}
resize(ctx.defers, defer_start)
resize(ctx.locals, scope_start)
return value, value_type
}
// build_value_source feeds a declaration/assignment RHS into the right value builder.
// Braced blocks are plain value blocks and must end in their own `yield`; bare RHS
// control flow (`x :: match ...`) sets `value_control_flow` and can produce directly.
// `label` is the labeled-block label (INVALID otherwise).
build_value_source :: proc(
ctx: ^Build_Ctx,
body: ^[dynamic]hir.Stmt_Id,
body_stmts: []ast.Stmt_Id,
expected: types.Type,
span: source.Span,
label := symbol.INVALID,
value_control_flow := false,
allow_exit := false,
) -> (value: hir.Expr_Id, value_type: types.Type) {
checker := ctx.checker
if symbol.is_valid(label) {
return build_value_labeled_block(ctx, body, body_stmts, label, expected, span)
}
if value_control_flow && len(body_stmts) == 1 {
#partial switch checker.ast_module.statements[body_stmts[0]].kind {
case .If:
return build_value_if(ctx, body, body_stmts[0], expected, span)
case .For, .While:
return build_value_loop(ctx, body, body_stmts[0], expected, span)
case .Match:
return build_value_match(ctx, body, body_stmts[0], expected, span)
}
}
return build_value_block(ctx, body, body_stmts, expected, span, allow_exit)
}
// new_value_slot allocates a fresh, un-nameable mutable local to hold a value-if/loop
// result. Branches/iterations assign it; the construct's value is a read of it.
new_value_slot :: proc(ctx: ^Build_Ctx, slot_type: types.Type) -> hir.Local_Id {
return append_tracked_local(
ctx.hir_locals,
ctx.local_spans,
ctx.local_used,
ctx.local_warnable,
hir.Local{name = ctx.checker.sink_symbol, type = slot_type, mutable = true},
source.Span{},
)
}
// slot_read builds a `.Local` read of a result slot.
slot_read :: proc(checker: ^Checker, slot: hir.Local_Id, slot_type: types.Type, span: source.Span) -> hir.Expr_Id {
id := hir.expr_id(len(checker.module.exprs))
append(&checker.module.exprs, hir.Expr{
kind = .Local, span = span, type = slot_type, target = hir.local_ref(slot),
})
return id
}
// emit_slot_assign appends a bare-local `slot = value` assignment to `out`.
emit_slot_assign :: proc(checker: ^Checker, out: ^[dynamic]hir.Stmt_Id, slot: hir.Local_Id, value: hir.Expr_Id, span: source.Span) {
append(out, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Assignment, span = span, expr = value, local = slot,
target = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC,
})
}
string_peer_slice_type :: proc(checker: ^Checker, value: types.Type) -> types.Type {
pointer, array, ok := types.array_pointer(value, &checker.module.types)
if ok && !pointer.mutable && !array.mutable &&
array.child == types.U8 && array.has_sentinel && array.sentinel == 0 {
return types.slice(&checker.module.types, types.U8, false, true, 0)
}
return types.INVALID
}
adopt_value_slot :: proc(
ctx: ^Build_Ctx,
slot: ^hir.Local_Id,
slot_type: ^types.Type,
value: hir.Expr_Id,
vtype: types.Type,
span: source.Span,
) -> hir.Expr_Id {
checker := ctx.checker
if slot^ == hir.INVALID_LOCAL {
peer := string_peer_slice_type(checker, vtype)
if types.is_valid(peer) {
slot_type^ = peer
slot^ = new_value_slot(ctx, slot_type^)
return coerce_expr(checker, value, slot_type^, span)
}
slot_type^ = vtype
slot^ = new_value_slot(ctx, slot_type^)
return value
}
return coerce_expr(checker, value, slot_type^, span)
}
// build_value_if turns `if c { … yield A } else { … yield B }` into a result slot
// each branch assigns, read after the if. Every path must yield: a mandatory `else`,
// each branch ends in `yield`, and all branches share a type (the first establishes it
// when untyped; later branches coerce). HIR holds an ordinary `.If` + a `.Local` read.
build_value_if :: proc(
ctx: ^Build_Ctx,
body: ^[dynamic]hir.Stmt_Id,
if_id: ast.Stmt_Id,
expected: types.Type,
span: source.Span,
) -> (value: hir.Expr_Id, value_type: types.Type) {
checker := ctx.checker
slot := hir.INVALID_LOCAL
slot_type := types.INVALID
if is_runtime_type(checker, expected) {
slot_type = expected
slot = new_value_slot(ctx, slot_type)
}
subtree: [dynamic]hir.Stmt_Id
subtree.allocator = checker.allocator
ok := emit_value_if(ctx, &subtree, if_id, &slot, &slot_type, span)
if !ok || slot == hir.INVALID_LOCAL {
for s in subtree {
append(body, s)
}
delete(subtree)
ctx.problematic^ = true
return invalid_hir_expr(checker, span, source.INVALID_DIAGNOSTIC), types.INVALID
}
// The slot's poison declaration precedes the if; every path assigns it.
append(body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Declaration, span = span, local = slot, expr = hir.INVALID_EXPR,
diagnostic = source.INVALID_DIAGNOSTIC,
})
for s in subtree {
append(body, s)
}
delete(subtree)
value = slot_read(checker, slot, slot_type, span)
return value, slot_type
}
// emit_value_if builds one `if`/`else if`/`else` level of a value-if, appending the
// assembled `.If` to `out`. `slot`/`slot_type` thread through so the first branch can
// fix an untyped slot and `else if` chains share it.
emit_value_if :: proc(
ctx: ^Build_Ctx,
out: ^[dynamic]hir.Stmt_Id,
if_id: ast.Stmt_Id,
slot: ^hir.Local_Id,
slot_type: ^types.Type,
span: source.Span,
) -> bool {
checker := ctx.checker
if_stmt := checker.ast_module.statements[if_id]
condition := hir.INVALID_EXPR
guard := hir.INVALID_EXPR
unwraps: []hir.Conditional_Unwrap = nil
capture_start := len(ctx.locals^)
if len(if_stmt.captures) > 0 {
// Unwrap value-if (`name :: if opt |v| { yield v } else { yield 0 }`): mirror the
// build-pass unwrap arm to bind captures + guard; each branch then assigns the slot
// like any other branch, and the HIR `.If` carries the unwraps (lowering handles it).
ast_operands: [dynamic]ast.Expr_Id
ast_operands.allocator = checker.allocator
flatten_conditional_unwrap_operands(checker.ast_module, if_stmt.expr, &ast_operands)
ok := true
if len(ast_operands) != len(if_stmt.captures) {
source.addf(checker.diagnostics, if_stmt.span,
"'if' unwrap has %d operands but %d captures", len(ast_operands), len(if_stmt.captures))
ok = false
}
values := make([]hir.Expr_Id, len(ast_operands), checker.allocator)
child_types := make([]types.Type, len(ast_operands), checker.allocator)
for operand, index in ast_operands {
child_types[index] = types.INVALID
v := build_expr(checker, operand, ctx.locals^[:], ctx.global_reads, ctx.calls, types.INVALID, ctx.pkg, ctx.file)
values[index] = v
vt := checker.module.exprs[v].type
if checker.module.exprs[v].kind == .Invalid {
ok = false
} else if !types.is_optional(vt, &checker.module.types) {
source.addf(checker.diagnostics, checker.ast_module.exprs[operand].span,
"'if' unwrap requires an optional value (operand %d)", index + 1)
ok = false
} else {
child_types[index] = types.child_type(vt, &checker.module.types)
}
}
unwrap_list: [dynamic]hir.Conditional_Unwrap
unwrap_list.allocator = checker.allocator
for capture, index in if_stmt.captures {
child := child_types[index] if index < len(child_types) else types.INVALID
local := hir.INVALID_LOCAL
if capture != checker.sink_symbol {
if _, dup := find_build_local(ctx.locals^[capture_start:], capture); dup {
source.add(checker.diagnostics, if_stmt.span, "'if' unwrap captures must have distinct names")
ok = false
} else if add_shadow_diagnostic(
checker, if_stmt.span, capture, "capture",
ctx.pkg, ctx.file, ctx.locals^[:capture_start], ctx.loop_labels^[:], ctx.yield_targets^[:],
) != source.INVALID_DIAGNOSTIC {
ok = false
}
local = append_build_local(ctx, capture, child, false, if_stmt.span)
}
if index < len(values) {
append(&unwrap_list, hir.Conditional_Unwrap{expr=values[index], local=local})
}
}
if if_stmt.guard != ast.INVALID_EXPR {
guard = build_expr(checker, if_stmt.guard, ctx.locals^[:], ctx.global_reads, ctx.calls, types.BOOL, ctx.pkg, ctx.file)
if checker.module.exprs[guard].kind == .Invalid {
ok = false
} else if !types.is_bool(checker.module.exprs[guard].type) &&
!types.is_noreturn(checker.module.exprs[guard].type) {
source.add(checker.diagnostics, checker.ast_module.exprs[if_stmt.guard].span, "'if' unwrap guard must be a bool")
ok = false
}
}
delete(values, checker.allocator)
delete(child_types, checker.allocator)
delete(ast_operands)
if !ok {
resize(ctx.locals, capture_start)
delete(unwrap_list)
ctx.problematic^ = true
return false
}
unwraps = unwrap_list[:]
} else {
condition = build_expr(checker, if_stmt.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, types.BOOL, ctx.pkg, ctx.file)
if checker.module.exprs[condition].kind != .Invalid &&
!types.is_bool(checker.module.exprs[condition].type) &&
!types.is_noreturn(checker.module.exprs[condition].type) {
id := source.add(checker.diagnostics, if_stmt.span, "'if' condition must be a bool")
condition = invalid_hir_expr(checker, if_stmt.span, id, types.BOOL)
ctx.problematic^ = true
}
}
// then-branch (unwrap captures, if any, are in scope here, then dropped before else).
then_body: [dynamic]hir.Stmt_Id
then_body.allocator = checker.allocator
then_ok := emit_value_branch(ctx, &then_body, if_stmt.body, slot, slot_type, span)
resize(ctx.locals, capture_start)
if !then_ok {
delete(then_body)
delete(unwraps)
return false
}
if if_stmt.else_body == nil {
source.add(checker.diagnostics, if_stmt.span, "an 'if' used as a value must have an 'else' so every path yields")
delete(then_body)
delete(unwraps)
ctx.problematic^ = true
return false
}
else_body: [dynamic]hir.Stmt_Id
else_body.allocator = checker.allocator
branch_ok := true
if len(if_stmt.else_body) == 1 && checker.ast_module.statements[if_stmt.else_body[0]].kind == .If {
branch_ok = emit_value_if(ctx, &else_body, if_stmt.else_body[0], slot, slot_type, span)
} else {
branch_ok = emit_value_branch(ctx, &else_body, if_stmt.else_body, slot, slot_type, span)
}
if !branch_ok {
delete(then_body)
delete(else_body)
delete(unwraps)
return false
}
append(out, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .If, span = if_stmt.span, expr = condition, guard = guard, unwraps = unwraps,
then_body = then_body[:], else_body = else_body[:],
local = hir.INVALID_LOCAL, target = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC,
})
return true
}
// emit_value_branch builds one branch of a value-if as a value block (leading stmts +
// trailing yield) and appends `slot = <value>`. The first branch of an untyped value-if
// fixes the slot type; later branches coerce to it (a mismatch is the "same type" error).
// A branch that does not yield is valid only if it exits on every path (return/break/
// continue) — it then produces no value and never reaches the slot read.
emit_value_branch :: proc(
ctx: ^Build_Ctx,
out: ^[dynamic]hir.Stmt_Id,
branch_stmts: []ast.Stmt_Id,
slot: ^hir.Local_Id,
slot_type: ^types.Type,
span: source.Span,
) -> bool {
checker := ctx.checker
n := len(branch_stmts)
if n == 1 && checker.ast_module.statements[branch_stmts[0]].kind == .Expression {
expr_stmt := checker.ast_module.statements[branch_stmts[0]]
expected := slot_type^ if slot^ != hir.INVALID_LOCAL else types.INVALID
value := build_expr(checker, expr_stmt.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, expected, ctx.pkg, ctx.file)
if checker.module.exprs[value].kind == .Invalid {
ctx.problematic^ = true
return false
}
value = adopt_value_slot(ctx, slot, slot_type, value, checker.module.exprs[value].type, span)
emit_slot_assign(checker, out, slot^, value, span)
return true
}
if n == 1 {
block_stmt := checker.ast_module.statements[branch_stmts[0]]
if block_stmt.kind == .Block && symbol.is_valid(block_stmt.label) {
expected := slot_type^ if slot^ != hir.INVALID_LOCAL else types.INVALID
value, value_type := build_value_labeled_block(
ctx, out, block_stmt.body, block_stmt.label, expected, block_stmt.span,
)
if checker.module.exprs[value].kind == .Invalid {
return false
}
value = adopt_value_slot(ctx, slot, slot_type, value, value_type, span)
emit_slot_assign(checker, out, slot^, value, span)
return true
}
}
ends_in_yield := n > 0 &&
checker.ast_module.statements[branch_stmts[n - 1]].kind == .Yield &&
!symbol.is_valid(checker.ast_module.statements[branch_stmts[n - 1]].label)
if !ends_in_yield {
// Not a value block: only allowed if every path exits (e.g. `else { return -1 }`),
// in which case it contributes no value to the slot.
built := build_block(ctx, branch_stmts)
for s in built {
append(out, s)
}
terminates := all_paths_exit(&checker.module, built)
delete(built, checker.allocator)
if terminates {
return true
}
source.add(checker.diagnostics, span,
"a value branch must end with 'yield' or exit on every path (return/break/continue)")
ctx.problematic^ = true
return false
}
value, vtype := build_value_block(ctx, out, branch_stmts, slot_type^, span)
if checker.module.exprs[value].kind == .Invalid {
return false
}
value = adopt_value_slot(ctx, slot, slot_type, value, vtype, span)
emit_slot_assign(checker, out, slot^, value, span)
return true
}
// Match_Built_Arm holds one already-built arm: its dispatch condition (`INVALID_EXPR`
// for the terminal `else`/exhaustive arm) and its body statements. The chain is
// assembled backward from these so diagnostics stay in source order.
Match_Built_Arm :: struct {
condition: hir.Expr_Id,
body: []hir.Stmt_Id,
terminal: bool,
}
// match_subject_location yields a fresh location expr for the `match` subject: a direct
// read of the value temp, or a deref of the pointer temp when an arm pointer-captures (so
// captures alias the original storage). Either way its lowered address is the subject's.
match_subject_location :: proc(checker: ^Checker, subj_local: hir.Local_Id, is_pointer: bool, subject_type, ptr_type: types.Type, span: source.Span) -> hir.Expr_Id {
if !is_pointer {
return slot_read(checker, subj_local, subject_type, span)
}
return add_hir_expr(checker, hir.Expr{
kind = .Deref, span = span, type = subject_type,
left = slot_read(checker, subj_local, ptr_type, span),
target = hir.INVALID_REF, right = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC,
})
}
// match_or ORs a fresh dispatch comparison into an arm's accumulating condition (for a
// multi-pattern arm), or returns it directly for the first pattern.
match_or :: proc(checker: ^Checker, condition, cmp: hir.Expr_Id, span: source.Span) -> hir.Expr_Id {
if condition == hir.INVALID_EXPR {
return cmp
}
return add_hir_expr(checker, hir.Expr{
kind = .Or, span = span, type = types.BOOL,
left = condition, right = cmp, target = hir.INVALID_REF, diagnostic = source.INVALID_DIAGNOSTIC,
})
}
// emit_match desugars a `match` into a single subject spill, one dispatch key read, and
// an `if`/`else if` chain. `as_value` (with `slot`/`slot_type`) routes each arm body
// through the value-branch machinery so the construct produces a value; otherwise arm
// bodies are ordinary statement blocks. Returns false (and emits a `.Trap`) on any error.
emit_match :: proc(
ctx: ^Build_Ctx,
out: ^[dynamic]hir.Stmt_Id,
statement: ast.Stmt,
as_value: bool,
slot: ^hir.Local_Id,
slot_type: ^types.Type,
) -> bool {
checker := ctx.checker
store := &checker.module.types
span := statement.span
fail :: proc(ctx: ^Build_Ctx, out: ^[dynamic]hir.Stmt_Id, span: source.Span, diagnostic: source.Diagnostic_Id) -> bool {
checker := ctx.checker
append(out, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Trap, span = span, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = diagnostic,
})
ctx.problematic^ = true
return false
}
// 1. Subject. A pointer capture (`|@cap|`) must alias the original storage, so when
// any arm requests one we spill the subject's *address* (it must be an addressable
// lvalue) and route reads through a deref; otherwise we spill the value as a copy.
// Either spill evaluates the subject exactly once.
subject := build_expr(checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, types.INVALID, ctx.pkg, ctx.file)
subject_type := checker.module.exprs[subject].type
if checker.module.exprs[subject].kind == .Invalid {
return fail(ctx, out, span, checker.module.exprs[subject].diagnostic)
}
is_tagged := types.is_tagged_union(subject_type, store)
is_enum_subject := types.is_enum(subject_type, store)
if types.is_union(subject_type, store) && !is_tagged {
return fail(ctx, out, span, source.add(checker.diagnostics, span, "cannot 'match' on an untagged union; it has no tag to dispatch on"))
}
if !is_tagged && !is_enum_subject && !types.is_concrete_scalar(subject_type) {
return fail(ctx, out, span, source.addf(checker.diagnostics, span,
"'match' subject must be a tagged union, enum, or scalar value, not '%s'", type_label(checker, subject_type)))
}
ok := true
wants_pointer := false
for arm_id in statement.body {
arm := checker.ast_module.statements[arm_id]
if arm.kind == .Match_Arm && arm.pointer_capture {
wants_pointer = true
break
}
}
subj_is_pointer := false
subj_writable := false
ptr_type := types.INVALID
if wants_pointer && is_tagged {
if hir_is_location(checker, subject) {
subj_is_pointer = true
subj_writable = hir_location_writable(checker, subject, ctx.locals^[:])
ptr_type = types.pointer(store, subject_type, subj_writable, false)
} else {
source.add(checker.diagnostics, span, "a pointer capture requires an addressable 'match' subject (bind it to a variable first)")
ok = false
}
}
spill_type := ptr_type if subj_is_pointer else subject_type
spill_value := subject
if subj_is_pointer {
spill_value = add_hir_expr(checker, hir.Expr{
kind = .Address, span = span, type = ptr_type, left = subject,
target = hir.INVALID_REF, right = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC,
})
}
subj_local := append_tracked_local(
ctx.hir_locals,
ctx.local_spans,
ctx.local_used,
ctx.local_warnable,
hir.Local{name = checker.sink_symbol, type = spill_type, mutable = false},
source.Span{},
)
append(out, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Declaration, span = span, local = subj_local, expr = spill_value,
diagnostic = source.INVALID_DIAGNOSTIC,
})
refinement_local := hir.INVALID_LOCAL
if checker.module.exprs[subject].kind == .Local {
candidate := hir.as_local(checker.module.exprs[subject].target)
if candidate != hir.INVALID_LOCAL && int(candidate) < len(ctx.hir_locals^) &&
!ctx.hir_locals^[candidate].mutable {
refinement_local = candidate
}
}
// 2. Dispatch key: a tagged union reads its discriminant into its own temp; an enum
// or scalar compares the subject directly.
key_local := subj_local
key_type := spill_type
tag_enum := types.INVALID
if is_tagged {
tag_enum = types.union_tag_enum(subject_type, store)
tag_read := add_hir_expr(checker, hir.Expr{
kind = .Union_Tag, span = span, type = tag_enum,
left = match_subject_location(checker, subj_local, subj_is_pointer, subject_type, ptr_type, span),
target = hir.INVALID_REF, right = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC,
})
tag_local := append_tracked_local(
ctx.hir_locals,
ctx.local_spans,
ctx.local_used,
ctx.local_warnable,
hir.Local{name = checker.sink_symbol, type = tag_enum, mutable = false},
source.Span{},
)
append(out, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Declaration, span = span, local = tag_local, expr = tag_read,
diagnostic = source.INVALID_DIAGNOSTIC,
})
key_local = tag_local
key_type = tag_enum
}
// 3. Build each arm (forward, for source-order diagnostics). An arm's `patterns` may
// list several alternatives (`.a, .b:` / `0, 1:`); their conditions are OR'd.
built: [dynamic]Match_Built_Arm
built.allocator = checker.allocator
defer delete(built)
covered: [dynamic]symbol.Id
covered.allocator = checker.allocator
defer delete(covered)
has_else := false
has_expand := false
for arm_id in statement.body {
arm := checker.ast_module.statements[arm_id]
if arm.kind != .Match_Arm {
ok = false
continue
}
if has_else || has_expand {
message := "arms after 'else' are unreachable" if has_else else "arms after 'inline' are unreachable"
source.add(checker.diagnostics, arm.span, message)
ok = false
}
if arm.expand {
if !is_tagged && !is_enum_subject {
source.add(checker.diagnostics, arm.span, "'inline' requires an enum or tagged-union match subject")
ok = false
continue
}
expected_captures := 1 if is_enum_subject else 2
if len(arm.captures) == 0 || len(arm.captures) > expected_captures {
description := "exactly one capture" if is_enum_subject else "one or two captures"
source.addf(checker.diagnostics, arm.span, "inlined match on '%s' requires %s", type_label(checker, subject_type), description)
ok = false
}
if is_enum_subject && arm.pointer_capture {
source.add(checker.diagnostics, arm.span, "enum inlining does not support pointer captures")
ok = false
}
if len(arm.captures) > 1 && arm.captures[0] != checker.sink_symbol && arm.captures[0] == arm.captures[1] {
source.add(checker.diagnostics, arm.span, "inline captures must have distinct names")
ok = false
}
remaining := 0
member_enum := tag_enum if is_tagged else subject_type
if is_tagged {
for field, field_index in types.fields_for(store, subject_type) {
name := symbol.Id(field.name)
if contains_name(covered[:], name) {
continue
}
member, found := find_enum_member(checker, member_enum, name)
if !found {
ok = false
continue
}
remaining += 1
append(&covered, name)
member_expr := enum_member_hir(checker, member_enum, name, arm.span)
condition := add_hir_expr(checker, hir.Expr{
kind=.Eq, span=arm.span, type=types.BOOL,
left=slot_read(checker, key_local, key_type, arm.span), right=member_expr,
target=hir.INVALID_REF, diagnostic=source.INVALID_DIAGNOSTIC,
})
static_start := len(checker.static_bindings)
if len(arm.captures) > 1 {
_ = push_static_integer_binding(checker, arm.captures[1], member_enum, member.value)
}
body_arm := arm
if types.is_void(field.type) && !arm.pointer_capture {
if len(arm.captures) > 0 {
_ = push_static_void_binding(checker, arm.captures[0])
}
body_arm.captures = nil
}
arm_body, body_ok := build_match_arm_body(
ctx, body_arm, subject_type, subj_local, subj_is_pointer, ptr_type, subj_writable,
field_index, field.type, refinement_local, []u32{u32(name)}, as_value, slot, slot_type, span,
)
pop_static_bindings(checker, static_start)
ok = body_ok && ok
append(&built, Match_Built_Arm{condition=condition, body=arm_body})
}
} else {
for member in types.enum_members_for(store, subject_type) {
name := symbol.Id(member.name)
if contains_name(covered[:], name) {
continue
}
remaining += 1
append(&covered, name)
member_expr := enum_member_hir(checker, subject_type, name, arm.span)
condition := add_hir_expr(checker, hir.Expr{
kind=.Eq, span=arm.span, type=types.BOOL,
left=slot_read(checker, key_local, key_type, arm.span), right=member_expr,
target=hir.INVALID_REF, diagnostic=source.INVALID_DIAGNOSTIC,
})
static_start := push_static_integer_binding(
checker, arm.captures[0] if len(arm.captures) > 0 else symbol.INVALID,
subject_type, member.value,
)
body_arm := arm
body_arm.captures = nil
arm_body, body_ok := build_match_arm_body(
ctx, body_arm, subject_type, subj_local, subj_is_pointer, ptr_type, subj_writable,
-1, types.INVALID, refinement_local, []u32{u32(name)}, as_value, slot, slot_type, span,
)
pop_static_bindings(checker, static_start)
ok = body_ok && ok
append(&built, Match_Built_Arm{condition=condition, body=arm_body})
}
}
if remaining == 0 {
source.add(checker.diagnostics, arm.span, "redundant 'inline': the 'match' already covers every variant")
ok = false
}
has_expand = true
continue
}
is_else := len(arm.patterns) == 0
condition := hir.INVALID_EXPR
field_index := -1
payload_type := types.INVALID
has_capture := len(arm.captures) > 0
refinement_variants: [dynamic]u32
refinement_variants.allocator = checker.allocator
if is_else {
if has_capture {
source.add(checker.diagnostics, arm.span, "the 'else' arm cannot capture a payload")
ok = false
}
has_else = true
member_enum := tag_enum if is_tagged else subject_type
for member in types.enum_members_for(store, member_enum) {
if !contains_name(covered[:], symbol.Id(member.name)) {
append(&refinement_variants, member.name)
}
}
} else if is_tagged || is_enum_subject {
// The capture payload (if any) must be one type across every listed variant.
capture_field := -1
capture_payload := types.INVALID
capture_conflict := types.INVALID
member_enum := tag_enum if is_tagged else subject_type
for pat_id in arm.patterns {
pattern := checker.ast_module.exprs[pat_id]
if pattern.kind == .Range {
source.add(checker.diagnostics, arm.span, "range patterns only apply to scalar 'match' subjects")
ok = false
continue
}
if pattern.kind != .Enum_Literal {
source.add(checker.diagnostics, arm.span, "an enum or tagged-union 'match' arm must be a '.variant' pattern")
ok = false
continue
}
if contains_name(covered[:], pattern.name) {
source.addf(checker.diagnostics, arm.span, "duplicate 'match' arm for '.%s'", symbol_text(checker, pattern.name))
ok = false
} else {
append(&covered, pattern.name)
append(&refinement_variants, u32(pattern.name))
}
if is_tagged {
index, field, found := find_struct_field(checker, subject_type, pattern.name)
if !found {
source.addf(checker.diagnostics, arm.span, "unknown variant '.%s' on '%s'", symbol_text(checker, pattern.name), type_label(checker, subject_type))
ok = false
continue
}
if capture_field < 0 {
capture_field = index
capture_payload = field.type
} else if !types.equal(capture_payload, field.type) {
capture_conflict = field.type
}
} else {
if _, found := find_enum_member(checker, subject_type, pattern.name); !found {
source.addf(checker.diagnostics, arm.span, "unknown member '.%s' on '%s'", symbol_text(checker, pattern.name), type_label(checker, subject_type))
ok = false
continue
}
}
member := enum_member_hir(checker, member_enum, pattern.name, arm.span)
cmp := add_hir_expr(checker, hir.Expr{
kind = .Eq, span = arm.span, type = types.BOOL,
left = slot_read(checker, key_local, key_type, arm.span), right = member,
target = hir.INVALID_REF, diagnostic = source.INVALID_DIAGNOSTIC,
})
condition = match_or(checker, condition, cmp, arm.span)
}
if has_capture {
if !is_tagged {
source.add(checker.diagnostics, arm.span, "only tagged-union variants can capture a payload")
ok = false
} else if types.is_valid(capture_conflict) {
source.addf(checker.diagnostics, arm.span, "capture group with incompatible types '%s' and '%s'",
type_label(checker, capture_payload), type_label(checker, capture_conflict))
ok = false
} else if types.is_void(capture_payload) {
source.add(checker.diagnostics, arm.span, "this variant has a void payload; there is nothing to capture")
ok = false
} else {
field_index = capture_field
payload_type = capture_payload
}
}
} else {
// Scalar subject: each pattern is a literal or a range, compared to the subject.
if has_capture {
source.add(checker.diagnostics, arm.span, "only tagged-union variants can capture a payload")
ok = false
}
for pat_id in arm.patterns {
pat_ast := checker.ast_module.exprs[pat_id]
cmp := hir.INVALID_EXPR
if pat_ast.kind == .Range {
lo := build_expr(checker, pat_ast.left, ctx.locals^[:], ctx.global_reads, ctx.calls, subject_type, ctx.pkg, ctx.file)
lo = coerce_expr(checker, lo, subject_type, arm.span)
hi := build_expr(checker, pat_ast.right, ctx.locals^[:], ctx.global_reads, ctx.calls, subject_type, ctx.pkg, ctx.file)
hi = coerce_expr(checker, hi, subject_type, arm.span)
if checker.module.exprs[lo].kind == .Invalid || checker.module.exprs[hi].kind == .Invalid {
ok = false
continue
}
ge := add_hir_expr(checker, hir.Expr{
kind = .Ge, span = arm.span, type = types.BOOL,
left = slot_read(checker, key_local, key_type, arm.span), right = lo,
target = hir.INVALID_REF, diagnostic = source.INVALID_DIAGNOSTIC,
})
hi_cmp := add_hir_expr(checker, hir.Expr{
kind = .Le if pat_ast.integer == 1 else .Lt, span = arm.span, type = types.BOOL,
left = slot_read(checker, key_local, key_type, arm.span), right = hi,
target = hir.INVALID_REF, diagnostic = source.INVALID_DIAGNOSTIC,
})
cmp = add_hir_expr(checker, hir.Expr{
kind = .And, span = arm.span, type = types.BOOL,
left = ge, right = hi_cmp, target = hir.INVALID_REF, diagnostic = source.INVALID_DIAGNOSTIC,
})
} else {
pattern := build_expr(checker, pat_id, ctx.locals^[:], ctx.global_reads, ctx.calls, subject_type, ctx.pkg, ctx.file)
pattern = coerce_expr(checker, pattern, subject_type, arm.span)
if checker.module.exprs[pattern].kind == .Invalid {
ok = false
continue
}
cmp = add_hir_expr(checker, hir.Expr{
kind = .Eq, span = arm.span, type = types.BOOL,
left = slot_read(checker, key_local, key_type, arm.span), right = pattern,
target = hir.INVALID_REF, diagnostic = source.INVALID_DIAGNOSTIC,
})
}
condition = match_or(checker, condition, cmp, arm.span)
}
}
arm_body, body_ok := build_match_arm_body(
ctx, arm, subject_type, subj_local, subj_is_pointer, ptr_type, subj_writable,
field_index, payload_type, refinement_local, refinement_variants[:], as_value, slot, slot_type, span,
)
delete(refinement_variants)
if !body_ok {
ok = false
}
append(&built, Match_Built_Arm{condition = condition, body = arm_body, terminal = is_else})
}
// 4. Exhaustiveness. Enum/union matches must cover every variant or supply `else`;
// an already-exhaustive match must not carry a redundant `else`. The last
// covered arm is promoted to the unconditional `else` so the chain terminates.
if is_tagged || is_enum_subject {
all_names := types.enum_members_for(store, tag_enum) if is_tagged else types.enum_members_for(store, subject_type)
names: [dynamic]symbol.Id
names.allocator = checker.allocator
defer delete(names)
if is_tagged {
for field in types.fields_for(store, subject_type) {
append(&names, symbol.Id(field.name))
}
} else {
for member in all_names {
append(&names, symbol.Id(member.name))
}
}
missing: [dynamic]symbol.Id
missing.allocator = checker.allocator
defer delete(missing)
for name in names {
if !contains_name(covered[:], name) {
append(&missing, name)
}
}
if has_else {
if len(missing) == 0 {
source.add(checker.diagnostics, span, "redundant 'else': the 'match' already covers every variant")
ok = false
}
} else if len(missing) > 0 {
builder: strings.Builder
strings.builder_init(&builder, checker.allocator)
defer strings.builder_destroy(&builder)
for name, index in missing {
if index > 0 {
strings.write_string(&builder, ", ")
}
strings.write_string(&builder, ".")
strings.write_string(&builder, symbol_text(checker, name))
}
source.addf(checker.diagnostics, span, "'match' on '%s' is not exhaustive; missing variants: %s (add the arms or an 'else')",
type_label(checker, subject_type), strings.to_string(builder))
ok = false
} else if len(built) > 0 {
built[len(built) - 1].terminal = true
}
} else if !has_else {
source.addf(checker.diagnostics, span, "a 'match' on '%s' requires an 'else' arm", type_label(checker, subject_type))
ok = false
}
if !ok {
// The arm bodies never get wired into the (un-assembled) chain, so free them here.
for arm in built {
delete(arm.body, checker.allocator)
}
return fail(ctx, out, span, source.INVALID_DIAGNOSTIC)
}
// 5. Assemble the if/else chain backward from the built arms. The terminal arm is the
// final (else / promoted) one; the rest nest as `if cond { body } else { … }`.
else_chain: []hir.Stmt_Id = nil
start := len(built)
if len(built) > 0 && built[len(built) - 1].terminal {
else_chain = built[len(built) - 1].body
start = len(built) - 1
}
for i := start - 1; i >= 0; i -= 1 {
arm := built[i]
wrapper := make([]hir.Stmt_Id, 1, checker.allocator)
wrapper[0] = hir.stmt_id(len(checker.module.statements))
append(&checker.module.statements, hir.Stmt{
kind = .If, span = span, expr = arm.condition, guard = hir.INVALID_EXPR,
then_body = arm.body, else_body = else_chain,
local = hir.INVALID_LOCAL, target = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC,
})
else_chain = wrapper
}
for s in else_chain {
append(out, s)
}
// The outermost chain slice's ids are now copied into `out`; the inner slices are
// owned by their enclosing `.If` (freed with the HIR module).
delete(else_chain, checker.allocator)
return true
}
// build_match_arm_body builds one arm's body, prefixed with the optional payload capture
// (`cap := subject.variant`, an unchecked reinterpret like a Zig union field read). For a
// statement match it is a plain block; for a value match each path assigns the result slot
// (a single-expression arm yields implicitly).
build_match_arm_body :: proc(
ctx: ^Build_Ctx,
arm: ast.Stmt,
subject_type: types.Type,
subj_local: hir.Local_Id,
subj_is_pointer: bool,
ptr_type: types.Type,
subj_writable: bool,
field_index: int,
payload_type: types.Type,
refinement_local: hir.Local_Id,
refinement_variants: []u32,
as_value: bool,
slot: ^hir.Local_Id,
slot_type: ^types.Type,
span: source.Span,
) -> ([]hir.Stmt_Id, bool) {
checker := ctx.checker
result: [dynamic]hir.Stmt_Id
result.allocator = checker.allocator
capture_start := len(ctx.locals^)
refinement_start := len(ctx.error_refinements^)
if refinement_local != hir.INVALID_LOCAL && len(refinement_variants) > 0 {
append(ctx.error_refinements, Error_Refinement{local=refinement_local, variants=refinement_variants})
}
capture_ok := true
if len(arm.captures) > 0 && field_index >= 0 {
capture := arm.captures[0]
if capture != checker.sink_symbol {
if id := add_shadow_diagnostic(
checker, span, capture, "capture",
ctx.pkg, ctx.file, ctx.locals^[:], ctx.loop_labels^[:], ctx.yield_targets^[:],
); id != source.INVALID_DIAGNOSTIC {
append(&result, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Trap, span = span, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = id,
})
capture_ok = false
}
// The payload sits at the subject's shared carrier offset. A value capture
// loads it; a `|@cap|` capture binds a pointer to it (mutability follows the
// subject), aliasing the original storage via the subject location.
field_read := add_hir_expr(checker, hir.Expr{
kind = .Field, span = span, type = payload_type, integer = i64(field_index),
left = match_subject_location(checker, subj_local, subj_is_pointer, subject_type, ptr_type, span),
target = hir.INVALID_REF, right = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC,
})
cap_type := payload_type
cap_value := field_read
if arm.pointer_capture {
cap_type = types.pointer(&checker.module.types, payload_type, subj_writable, false)
cap_value = add_hir_expr(checker, hir.Expr{
kind = .Address, span = span, type = cap_type, left = field_read,
target = hir.INVALID_REF, right = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC,
})
}
cap_local := append_build_local(ctx, capture, cap_type, false, span)
append(&result, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Declaration, span = span, local = cap_local, expr = cap_value,
diagnostic = source.INVALID_DIAGNOSTIC,
})
}
}
body_ok := capture_ok
if !as_value {
built := build_block(ctx, arm.body)
for s in built {
append(&result, s)
}
delete(built, checker.allocator)
} else {
body_ok = build_value_arm(ctx, &result, arm.body, slot, slot_type, span) && body_ok
}
resize(ctx.locals, capture_start)
resize(ctx.error_refinements, refinement_start)
return result[:], body_ok
}
// build_value_arm appends a value-match arm's slot assignment(s) to `out`.
build_value_arm :: proc(
ctx: ^Build_Ctx,
out: ^[dynamic]hir.Stmt_Id,
arm_body: []ast.Stmt_Id,
slot: ^hir.Local_Id,
slot_type: ^types.Type,
span: source.Span,
) -> bool {
return emit_value_branch(ctx, out, arm_body, slot, slot_type, span)
}
// build_match desugars a statement-position `match` into its if/else chain.
build_match :: proc(ctx: ^Build_Ctx, body: ^[dynamic]hir.Stmt_Id, statement: ast.Stmt) {
slot := hir.INVALID_LOCAL
slot_type := types.INVALID
emit_match(ctx, body, statement, false, &slot, &slot_type)
}
// build_value_match desugars a `match` used as a declaration/assignment RHS: a result slot
// each arm assigns, read after the chain. Mirrors build_value_if.
build_value_match :: proc(
ctx: ^Build_Ctx,
body: ^[dynamic]hir.Stmt_Id,
match_id: ast.Stmt_Id,
expected: types.Type,
span: source.Span,
) -> (value: hir.Expr_Id, value_type: types.Type) {
checker := ctx.checker
statement := checker.ast_module.statements[match_id]
slot := hir.INVALID_LOCAL
slot_type := types.INVALID
if is_runtime_type(checker, expected) {
slot_type = expected
slot = new_value_slot(ctx, slot_type)
}
subtree: [dynamic]hir.Stmt_Id
subtree.allocator = checker.allocator
ok := false
if selected_body, capture, has_capture, comptime_ok := specialization_match_body(
checker, statement, ctx.pkg, ctx.file,
); comptime_ok {
if has_capture {
append(&checker.static_bindings, capture)
}
ok = build_value_arm(ctx, &subtree, selected_body, &slot, &slot_type, span)
if has_capture {
_ = pop(&checker.static_bindings)
}
} else {
ok = emit_match(ctx, &subtree, statement, true, &slot, &slot_type)
}
if !ok || slot == hir.INVALID_LOCAL {
for s in subtree {
append(body, s)
}
delete(subtree)
ctx.problematic^ = true
return invalid_hir_expr(checker, span, source.INVALID_DIAGNOSTIC), types.INVALID
}
append(body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Declaration, span = span, local = slot, expr = hir.INVALID_EXPR,
diagnostic = source.INVALID_DIAGNOSTIC,
})
for s in subtree {
append(body, s)
}
delete(subtree)
return slot_read(checker, slot, slot_type, span), slot_type
}
// loop_yields_null reports whether any `yield` that targets this loop (a labeled
// `yield :blk` inside `if`/block branches, or the trailing fall-through) yields the
// literal `null` — making the loop's result optional. Pure AST walk; does not descend
// into nested loops or value sources, whose yields belong to them.
loop_yields_null :: proc(checker: ^Checker, stmts: []ast.Stmt_Id) -> bool {
for id in stmts {
s := checker.ast_module.statements[id]
#partial switch s.kind {
case .Yield:
if s.expr != ast.INVALID_EXPR && checker.ast_module.exprs[s.expr].kind == .Null {
return true
}
case .If:
if loop_yields_null(checker, s.body) || loop_yields_null(checker, s.else_body) {
return true
}
case .Block:
if loop_yields_null(checker, s.body) {
return true
}
}
}
return false
}
// resolve_loop_slot fixes a value-loop's result slot from its first concrete yield (an
// optional element type when the loop also yields `null`) and coerces `value` into it.
// Returns INVALID when the type can't be fixed yet (a `null`/invalid first yield).
resolve_loop_slot :: proc(ctx: ^Build_Ctx, target: ^Yield_Target, value: hir.Expr_Id, vtype: types.Type, span: source.Span) -> hir.Expr_Id {
checker := ctx.checker
if target.slot == hir.INVALID_LOCAL {
if !is_runtime_type(checker, vtype) {
return hir.INVALID_EXPR
}
target.slot_type = types.optional(&checker.module.types, vtype) if target.result_optional else vtype
target.slot = new_value_slot(ctx, target.slot_type)
}
return coerce_expr(checker, value, target.slot_type, span)
}
// first_concrete_yield_expr returns the AST expr of the first yield (source order) that is
// not the literal `null`, descending into `if`/block branches but not nested loops or value
// sources (whose yields belong to them). INVALID when the loop yields only `null`.
first_concrete_yield_expr :: proc(checker: ^Checker, stmts: []ast.Stmt_Id) -> ast.Expr_Id {
for id in stmts {
s := checker.ast_module.statements[id]
#partial switch s.kind {
case .Yield:
if s.expr != ast.INVALID_EXPR && checker.ast_module.exprs[s.expr].kind != .Null {
return s.expr
}
case .If:
if e := first_concrete_yield_expr(checker, s.body); e != ast.INVALID_EXPR {
return e
}
if e := first_concrete_yield_expr(checker, s.else_body); e != ast.INVALID_EXPR {
return e
}
case .Block:
if e := first_concrete_yield_expr(checker, s.body); e != ast.INVALID_EXPR {
return e
}
}
}
return ast.INVALID_EXPR
}
// value_loop_element_type pre-types the element of an untyped value loop from its first
// concrete (non-`null`) yield, so a `null` yielded before any concrete value still resolves
// the result to `?T`. The loop's captures are bound temporarily for the probe and the probe
// expr is discarded; returns INVALID when the loop yields only `null`.
value_loop_element_type :: proc(ctx: ^Build_Ctx, loop_stmt: ast.Stmt) -> types.Type {
checker := ctx.checker
yield_expr := first_concrete_yield_expr(checker, loop_stmt.body)
if yield_expr == ast.INVALID_EXPR {
return types.INVALID
}
capture_start := len(ctx.locals^)
local_start := len(ctx.hir_locals^)
if loop_stmt.kind == .For {
// Mirror the `.For` arm's capture-type computation just enough to type the probe.
iterable := build_expr(checker, loop_stmt.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, types.INVALID, ctx.pkg, ctx.file)
iterable_type := checker.module.exprs[iterable].type
capture_type := types.INVALID
if types.is_range(iterable_type, &checker.module.types) {
capture_type = types.child_type(iterable_type, &checker.module.types)
} else if item, ok := sequence_item(iterable_type, &checker.module.types); ok {
capture_type = item.child
if loop_stmt.pointer_capture {
capture_type = types.pointer(&checker.module.types, item.child, item.mutable, false)
}
}
if symbol.is_valid(loop_stmt.name) {
append_build_local(ctx, loop_stmt.name, capture_type, false, loop_stmt.span)
}
if symbol.is_valid(loop_stmt.index_name) {
append_build_local(ctx, loop_stmt.index_name, types.USIZE, false, loop_stmt.span)
}
}
probe := build_expr(checker, yield_expr, ctx.locals^[:], ctx.global_reads, ctx.calls, types.INVALID, ctx.pkg, ctx.file)
result := checker.module.exprs[probe].type if checker.module.exprs[probe].kind != .Invalid else types.INVALID
resize(ctx.locals, capture_start)
ignore_tracked_locals(ctx, local_start)
return result
}
// stmt_contains_yield reports whether a statement contains a `yield` anywhere within it
// (recursing through if/block/loop bodies). Used to stop the leading probe build before any
// statement that yields (the block's yield target is not pushed during the probe).
stmt_contains_yield :: proc(checker: ^Checker, id: ast.Stmt_Id) -> bool {
s := checker.ast_module.statements[id]
#partial switch s.kind {
case .Yield:
return true
case .If, .For, .While, .Block:
for sub in s.body {
if stmt_contains_yield(checker, sub) {
return true
}
}
for sub in s.else_body {
if stmt_contains_yield(checker, sub) {
return true
}
}
}
return false
}
// block_element_type pre-types an untyped value block's element from its first concrete
// `yield :blk`, building the block's leading (yield-free) statements first so the probe can
// reference block locals declared before the first yield. The leading build is a throwaway
// (its scope is restored). INVALID when there is no concrete yield, or the concrete yield
// references a local only in scope past the first yield (annotate the binding instead).
block_element_type :: proc(ctx: ^Build_Ctx, block_stmts: []ast.Stmt_Id) -> types.Type {
checker := ctx.checker
concrete := first_concrete_yield_expr(checker, block_stmts)
if concrete == ast.INVALID_EXPR {
return types.INVALID
}
lead_end := len(block_stmts)
for id, i in block_stmts {
if stmt_contains_yield(checker, id) {
lead_end = i
break
}
}
scope_start := len(ctx.locals^)
local_start := len(ctx.hir_locals^)
defer_start := len(ctx.defers^)
lead := build_block(ctx, block_stmts[:lead_end], close = false)
delete(lead, checker.allocator)
probe := build_expr(checker, concrete, ctx.locals^[:], ctx.global_reads, ctx.calls, types.INVALID, ctx.pkg, ctx.file)
result := checker.module.exprs[probe].type if checker.module.exprs[probe].kind != .Invalid else types.INVALID
// Discard the throwaway leading build's scope (its hir stmts/locals are dead but stable).
for i := defer_start; i < len(ctx.defers^); i += 1 {
delete(ctx.defers^[i].body, checker.allocator)
}
resize(ctx.defers, defer_start)
resize(ctx.locals, scope_start)
ignore_tracked_locals(ctx, local_start)
return result
}
// build_value_labeled_block turns `x :: blk: { …; yield :blk v }` into a result slot each
// `yield :blk` assigns (via the build_block `.Yield` desugar → `slot = v; break :blk`), then
// reads it after the block. Every path must yield (or otherwise exit); HIR holds a `.Block`
// that emits the body and the exit label the labeled breaks branch to. No iteration / no
// fall-through (unlike a value loop). The type is the annotation when typed, else the first
// concrete `yield :blk`'s type (optional when any yield is `null`).
build_value_labeled_block :: proc(
ctx: ^Build_Ctx,
body: ^[dynamic]hir.Stmt_Id,
block_stmts: []ast.Stmt_Id,
label: symbol.Id,
expected: types.Type,
span: source.Span,
) -> (value: hir.Expr_Id, value_type: types.Type) {
checker := ctx.checker
result_optional := loop_yields_null(checker, block_stmts)
slot := hir.INVALID_LOCAL
slot_type := types.INVALID
if is_runtime_type(checker, expected) {
slot_type = expected
slot = new_value_slot(ctx, slot_type)
result_optional = types.is_optional(slot_type, &checker.module.types)
} else if result_optional {
// Untyped block that also yields `null`: pre-type the element from the first
// concrete yield (regardless of source order) so a `null` yielded first still
// resolves the result to `?T`.
elem := block_element_type(ctx, block_stmts)
if is_runtime_type(checker, elem) {
slot_type = types.optional(&checker.module.types, elem)
slot = new_value_slot(ctx, slot_type)
}
}
if id := add_label_shadow_diagnostic(ctx, span, label); id != source.INVALID_DIAGNOSTIC {
ctx.problematic^ = true
return invalid_hir_expr(checker, span, id), types.INVALID
}
append(ctx.yield_targets, Yield_Target{
label = label, slot = slot, slot_type = slot_type, result_optional = result_optional,
defer_floor = len(ctx.defers^),
})
built := build_block(ctx, block_stmts)
target := pop(ctx.yield_targets)
if target.slot == hir.INVALID_LOCAL {
for s in built {
append(body, s)
}
delete(built, checker.allocator)
id := source.add(checker.diagnostics, span,
"could not determine the value block's yield type; annotate the binding")
ctx.problematic^ = true
return invalid_hir_expr(checker, span, id), types.INVALID
}
// Every path must `yield :blk` (or return/break out); otherwise a path falls through
// to the slot read with a poison value.
if !all_paths_exit(&checker.module, built) {
for s in built {
append(body, s)
}
delete(built, checker.allocator)
id := source.add(checker.diagnostics, span, "a labeled value block must 'yield' on every path")
ctx.problematic^ = true
return invalid_hir_expr(checker, span, id), types.INVALID
}
append(body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Declaration, span = span, local = target.slot, expr = hir.INVALID_EXPR,
diagnostic = source.INVALID_DIAGNOSTIC,
})
append(body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Block, span = span, label = label, then_body = built,
local = hir.INVALID_LOCAL, target = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC,
})
value = slot_read(checker, target.slot, target.slot_type, span)
return value, target.slot_type
}
// build_value_loop turns a labeled `for/while ... blk: { … }` whose body ends in a
// fall-through `yield` (and may early-exit via `yield :blk x`) into a result slot:
// the fall-through value initializes the slot before the loop, each `yield :blk x`
// desugars (in build_block) to `slot = x; break`, and the construct's value is a read
// of the slot after the loop. Reuses the ordinary `.For`/`.While` build via a peeled
// copy; no new HIR. The yielded type is the annotation when typed, else the first
// concrete yield's type (optional when any yield is `null`).
build_value_loop :: proc(
ctx: ^Build_Ctx,
body: ^[dynamic]hir.Stmt_Id,
loop_id: ast.Stmt_Id,
expected: types.Type,
span: source.Span,
) -> (value: hir.Expr_Id, value_type: types.Type) {
checker := ctx.checker
loop_stmt := checker.ast_module.statements[loop_id]
n := len(loop_stmt.body)
last_is_fallthrough := n > 0 &&
checker.ast_module.statements[loop_stmt.body[n - 1]].kind == .Yield &&
!symbol.is_valid(checker.ast_module.statements[loop_stmt.body[n - 1]].label)
if !symbol.is_valid(loop_stmt.label) {
id := source.add(checker.diagnostics, span,
"a value loop must label its body (e.g. 'blk:') so a 'yield :blk' can exit it")
ctx.problematic^ = true
return invalid_hir_expr(checker, span, id), types.INVALID
}
if !last_is_fallthrough {
id := source.add(checker.diagnostics, span,
"a value loop's body must end with a 'yield' for when the loop completes")
ctx.problematic^ = true
return invalid_hir_expr(checker, span, id), types.INVALID
}
fall_stmt := checker.ast_module.statements[loop_stmt.body[n - 1]]
result_optional := loop_yields_null(checker, loop_stmt.body)
slot := hir.INVALID_LOCAL
slot_type := types.INVALID
if is_runtime_type(checker, expected) {
slot_type = expected
slot = new_value_slot(ctx, slot_type)
result_optional = types.is_optional(slot_type, &checker.module.types)
} else if result_optional {
// Untyped loop that also yields `null`: pre-type the element from the first
// concrete yield (regardless of source order) so a `null` built before any
// concrete yield still resolves the result to `?T`.
elem := value_loop_element_type(ctx, loop_stmt)
if is_runtime_type(checker, elem) {
slot_type = types.optional(&checker.module.types, elem)
slot = new_value_slot(ctx, slot_type)
}
}
append(ctx.yield_targets, Yield_Target{
label = loop_stmt.label, slot = slot, slot_type = slot_type, result_optional = result_optional,
defer_floor = len(ctx.defers^),
})
// Build the loop with the fall-through peeled off, reusing the normal For/While arm.
// The peeled body is a fresh copy so destroy_module won't double-free the original.
peeled := loop_stmt
peeled_body := make([]ast.Stmt_Id, n - 1, checker.ast_module.allocator)
copy(peeled_body, loop_stmt.body[:n - 1])
peeled.body = peeled_body
peeled_id := ast.stmt_id(len(checker.ast_module.statements))
append(&checker.ast_module.statements, peeled)
loop_block := build_block(ctx, []ast.Stmt_Id{peeled_id})
target := pop(ctx.yield_targets)
// The fall-through value initializes the slot before the loop (loop captures are
// out of scope here), so the loop completing leaves it as the result.
fall_value := build_expr(checker, fall_stmt.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, target.slot_type, ctx.pkg, ctx.file)
if fall_value != hir.INVALID_EXPR && types.is_void(checker.module.exprs[fall_value].type) {
for s in loop_block {
append(body, s)
}
delete(loop_block, checker.allocator)
id := source.add(checker.diagnostics, fall_stmt.span, "'yield' expression must produce a non-void value")
ctx.problematic^ = true
return invalid_hir_expr(checker, fall_stmt.span, id), types.INVALID
}
fall_value = resolve_loop_slot(ctx, &target, fall_value, checker.module.exprs[fall_value].type if fall_value != hir.INVALID_EXPR else types.INVALID, fall_stmt.span)
if target.slot == hir.INVALID_LOCAL || fall_value == hir.INVALID_EXPR {
for s in loop_block {
append(body, s)
}
delete(loop_block, checker.allocator)
id := source.add(checker.diagnostics, span,
"could not determine the value loop's yield type; annotate the binding")
ctx.problematic^ = true
return invalid_hir_expr(checker, span, id), types.INVALID
}
append(body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Declaration, span = span, local = target.slot, expr = fall_value,
diagnostic = source.INVALID_DIAGNOSTIC,
})
for s in loop_block {
append(body, s)
}
delete(loop_block, checker.allocator)
value = slot_read(checker, target.slot, target.slot_type, span)
return value, target.slot_type
}
Return_Enum_Guard :: struct {
local: hir.Local_Id,
type: types.Type,
value: i64,
}
return_enum_guard :: proc(
module: ^hir.Module,
statement: hir.Stmt,
locals: []hir.Local,
) -> (Return_Enum_Guard, bool) {
if statement.kind != .If || statement.else_body != nil ||
statement.expr == hir.INVALID_EXPR || int(statement.expr) >= len(module.exprs) {
return {}, false
}
condition := module.exprs[statement.expr]
if condition.kind != .Eq || condition.left == hir.INVALID_EXPR || condition.right == hir.INVALID_EXPR ||
int(condition.left) >= len(module.exprs) || int(condition.right) >= len(module.exprs) {
return {}, false
}
left := module.exprs[condition.left]
right := module.exprs[condition.right]
if left.kind == .Integer {
left, right = right, left
}
if left.kind != .Local || right.kind != .Integer ||
!types.equal(left.type, right.type) || !types.is_enum(left.type, &module.types) {
return {}, false
}
local := hir.as_local(left.target)
if local == hir.INVALID_LOCAL || int(local) >= len(locals) || locals[local].mutable {
return {}, false
}
if !all_paths_return(module, statement.then_body, locals) {
return {}, false
}
return Return_Enum_Guard{local=local, type=left.type, value=right.integer}, true
}
enum_guards_return :: proc(module: ^hir.Module, stmts: []hir.Stmt_Id, locals: []hir.Local) -> bool {
guards: [dynamic]Return_Enum_Guard
guards.allocator = module.allocator
defer delete(guards)
for id in stmts {
if guard, ok := return_enum_guard(module, module.statements[id], locals); ok {
append(&guards, guard)
}
}
for guard, index in guards {
seen := false
for previous in guards[:index] {
seen = seen || previous.local == guard.local
}
if seen {
continue
}
members := types.enum_members_for(&module.types, guard.type)
if len(members) == 0 {
continue
}
complete := true
for member in members {
value := i64(member.value) if member.value < 0 else transmute(i64)u64(member.value)
covered := false
for candidate in guards {
covered = covered || candidate.local == guard.local && candidate.value == value
}
complete = complete && covered
}
if complete {
return true
}
}
return false
}
// 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.
// Exhaustive equality guards over one immutable enum local also terminate collectively.
all_paths_return :: proc(module: ^hir.Module, stmts: []hir.Stmt_Id, locals: []hir.Local = nil) -> bool {
for id in stmts {
statement := module.statements[id]
if statement.expr != hir.INVALID_EXPR && int(statement.expr) < len(module.exprs) &&
types.is_noreturn(module.exprs[statement.expr].type) {
return true
}
#partial switch statement.kind {
case .Return, .Trap:
return true
case .If:
if statement.else_body != nil &&
all_paths_return(module, statement.then_body, locals) &&
all_paths_return(module, statement.else_body, locals) {
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 enum_guards_return(module, stmts, locals)
}
// Reports whether `stmts` contains a `break` that targets the enclosing loop:
// a `.Break` at this level or inside `if`/`else` branches counts, but a `break`
// inside a nested `.While`/`.For` targets that inner loop, so we do not descend.
loop_body_breaks :: proc(module: ^hir.Module, stmts: []hir.Stmt_Id) -> bool {
for id in stmts {
statement := module.statements[id]
#partial switch statement.kind {
case .Break:
return true
case .If:
if loop_body_breaks(module, statement.then_body) ||
loop_body_breaks(module, statement.else_body) {
return true
}
}
}
return false
}
// Like `all_paths_return`, but also treats `break`/`continue` as terminating the
// block. Used only to decide whether `build_block` may skip the fall-through defer
// flush (a block that always exits early would otherwise emit unreachable copies).
all_paths_exit :: proc(module: ^hir.Module, stmts: []hir.Stmt_Id) -> bool {
for id in stmts {
statement := module.statements[id]
if statement.expr != hir.INVALID_EXPR && int(statement.expr) < len(module.exprs) &&
types.is_noreturn(module.exprs[statement.expr].type) {
return true
}
#partial switch statement.kind {
case .Return, .Trap, .Break, .Continue:
return true
case .If:
if statement.else_body != nil &&
all_paths_exit(module, statement.then_body) &&
all_paths_exit(module, statement.else_body) {
return true
}
case .While:
if statement.expr != hir.INVALID_EXPR && int(statement.expr) < len(module.exprs) {
condition := module.exprs[statement.expr]
if condition.kind == .Bool && condition.integer != 0 &&
!loop_body_breaks(module, statement.then_body) {
return true
}
}
}
}
return false
}
build_function :: proc(checker: ^Checker, id: Spec_Id) {
spec := checker.specs[id]
function := checker.ast_module.functions[spec.template]
previous_comptime := checker.current_comptime_values
checker.current_comptime_values = spec.comptime_values
defer checker.current_comptime_values = previous_comptime
signature_diagnostic := source.INVALID_DIAGNOSTIC
unresolved_result := !types.is_void(spec.result) && !types.is_noreturn(spec.result) && !is_runtime_type(checker, spec.result)
if unresolved_result {
if types.is_comptime_only(spec.result, &checker.module.types) {
signature_diagnostic = source.addf(
checker.diagnostics,
function.span,
"function '%s' has a comptime-only result and cannot return it through the runtime ABI",
symbol_text(checker, function.name),
)
}
checker.specs[id].result = types.I64
spec.result = types.I64
}
for arg in spec.args {
if !is_runtime_type(checker, arg) {
signature_diagnostic = source.addf(
checker.diagnostics,
function.span,
"could not resolve a concrete parameter type for '%s'",
symbol_text(checker, function.name),
)
break
}
}
assert(spec.hir_id == hir.function_id(len(checker.module.functions)))
locals: [dynamic]Build_Local
locals.allocator = checker.allocator
hir_locals: [dynamic]hir.Local
hir_locals.allocator = checker.allocator
local_spans: [dynamic]source.Span
local_spans.allocator = checker.allocator
local_used: [dynamic]bool
local_used.allocator = checker.allocator
local_warnable: [dynamic]bool
local_warnable.allocator = checker.allocator
params: [dynamic]hir.Local_Id
params.allocator = checker.allocator
body: [dynamic]hir.Stmt_Id
body.allocator = checker.allocator
global_reads: [dynamic]hir.Global_Id
global_reads.allocator = checker.allocator
calls: [dynamic]hir.Function_Id
calls.allocator = checker.allocator
demanded: [dynamic]Spec_Id
demanded.allocator = checker.allocator
local_types, _ := infer_spec_locals_and_result(checker, id, &demanded)
defer {
delete(local_types, checker.allocator)
delete(demanded)
}
runtime_index := 0
for param in function.params {
if param.comptime_value {
continue
}
param_type := types.INVALID
if runtime_index < len(spec.args) {
param_type = spec.args[runtime_index]
}
local_id := append_tracked_local(
&hir_locals,
&local_spans,
&local_used,
&local_warnable,
hir.Local{name = param.name, type = param_type, parameter = true},
param.span,
)
if param.name != checker.sink_symbol && block_reads_name(checker, function.body, param.name) {
local_used[int(local_id)] = true
}
append(&locals, Build_Local{name = param.name, type = param_type, id = local_id})
append(&params, local_id)
runtime_index += 1
}
problematic := signature_diagnostic != source.INVALID_DIAGNOSTIC ||
checker.template_diagnostics[spec.template] != source.INVALID_DIAGNOSTIC
native_main := function.pkg == 0 && function.name == checker.main_symbol &&
checker.entry_point == .Plain &&
types.kind(spec.result, &checker.module.types) != .Fallible
if !function.has_body {
if unresolved_result && signature_diagnostic == source.INVALID_DIAGNOSTIC &&
checker.template_diagnostics[spec.template] == source.INVALID_DIAGNOSTIC {
signature_diagnostic = source.addf(
checker.diagnostics,
function.span,
"could not resolve a concrete result type for '%s'",
symbol_text(checker, function.name),
)
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 else .Brolang,
implementation = .Declaration,
linkage = .External if function.c_abi else .Internal,
is_main = native_main,
variadic = function.variadic,
params = params[:],
result = spec.result,
locals = hir_locals[:],
body = body[:],
direct_global_reads = global_reads,
calls = calls[:],
problematic = problematic,
diagnostic = checker.template_diagnostics[spec.template],
},
)
delete(locals)
delete(local_spans)
delete(local_used)
delete(local_warnable)
return
}
if signature_diagnostic != source.INVALID_DIAGNOSTIC {
append(&body, hir.stmt_id(len(checker.module.statements)))
append(
&checker.module.statements,
hir.Stmt {
kind = .Trap,
span = function.span,
expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL,
diagnostic = signature_diagnostic,
},
)
}
defers: [dynamic]Defer_Entry
defers.allocator = checker.allocator
loop_defer_starts: [dynamic]int
loop_defer_starts.allocator = checker.allocator
loop_labels: [dynamic]symbol.Id
loop_labels.allocator = checker.allocator
loop_is_loop: [dynamic]bool
loop_is_loop.allocator = checker.allocator
yield_targets: [dynamic]Yield_Target
yield_targets.allocator = checker.allocator
error_refinements: [dynamic]Error_Refinement
error_refinements.allocator = checker.allocator
ctx := Build_Ctx{
checker = checker,
pkg = function.pkg,
file = function.file,
result = spec.result,
local_types = local_types,
locals = &locals,
hir_locals = &hir_locals,
local_spans = &local_spans,
local_used = &local_used,
local_warnable = &local_warnable,
global_reads = &global_reads,
calls = &calls,
problematic = &problematic,
error_refinements = &error_refinements,
defers = &defers,
loop_defer_starts = &loop_defer_starts,
loop_labels = &loop_labels,
loop_is_loop = &loop_is_loop,
yield_targets = &yield_targets,
}
previous_result := checker.current_result
previous_ctx := checker.current_build_ctx
checker.current_result = spec.result
checker.current_build_ctx = &ctx
block := build_block(&ctx, function.body)
checker.current_result = previous_result
checker.current_build_ctx = previous_ctx
returns := all_paths_return(&checker.module, block, hir_locals[:])
for block_stmt in block {
append(&body, block_stmt)
}
delete(block, checker.allocator)
if unresolved_result && signature_diagnostic == source.INVALID_DIAGNOSTIC &&
checker.template_diagnostics[spec.template] == source.INVALID_DIAGNOSTIC && !problematic {
signature_diagnostic = source.addf(
checker.diagnostics,
function.span,
"could not resolve a concrete result type for '%s'",
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=signature_diagnostic,
})
problematic = true
returns = true
}
fallible_void := types.kind(spec.result, &checker.module.types) == .Fallible &&
types.is_void(types.fallible_success(spec.result, &checker.module.types))
if !returns && fallible_void {
value := fallible_aggregate(checker, function.span, spec.result, hir.INVALID_EXPR, false)
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind=.Return, span=function.span, expr=value, local=hir.INVALID_LOCAL,
diagnostic=source.INVALID_DIAGNOSTIC,
})
} else if !types.is_void(spec.result) && !returns {
id := source.addf(
checker.diagnostics,
function.span,
"function '%s' does not return a value",
symbol_text(checker, function.name),
)
append(&body, hir.stmt_id(len(checker.module.statements)))
append(
&checker.module.statements,
hir.Stmt{kind = .Trap, span = function.span, expr = hir.INVALID_EXPR, local = hir.INVALID_LOCAL, diagnostic = id},
)
problematic = true
}
record_unused_locals(checker, hir_locals[:], local_spans[:], local_used[:], local_warnable[:])
assert(spec.hir_id == hir.function_id(len(checker.module.functions)))
append(
&checker.module.functions,
hir.Function {
name = function.name,
link_name = make_link_name(checker, id),
calling_convention = .C if function.c_abi || native_main else .Brolang,
implementation = .Definition,
linkage = .External if function.c_abi || native_main else .Internal,
is_main = native_main,
variadic = function.variadic,
params = params[:],
result = spec.result,
locals = hir_locals[:],
body = body[:],
direct_global_reads = global_reads,
calls = calls[:],
problematic = problematic,
diagnostic = source.INVALID_DIAGNOSTIC,
},
)
for entry in defers {
delete(entry.body, checker.allocator)
}
delete(defers)
delete(loop_defer_starts)
delete(loop_labels)
delete(loop_is_loop)
delete(yield_targets)
delete(error_refinements)
delete(locals)
delete(local_spans)
delete(local_used)
delete(local_warnable)
}
expr_problematic :: proc(checker: ^Checker, expr_id: hir.Expr_Id) -> bool {
module := &checker.module
stack := checker.hir_expr_stack
clear_dynamic_array(&stack)
defer {
clear_dynamic_array(&stack)
checker.hir_expr_stack = stack
}
append(&stack, expr_id)
for len(stack) > 0 {
id := pop(&stack)
if id == hir.INVALID_EXPR || int(id) >= len(module.exprs) {
return true
}
expr := module.exprs[id]
if expr.kind == .Invalid {
return true
}
if expr.left != hir.INVALID_EXPR {
append(&stack, expr.left)
}
if expr.right != hir.INVALID_EXPR {
append(&stack, expr.right)
}
append(&stack, ..expr.args)
}
return false
}
static_integer_value :: proc(module: ^hir.Module, expr_id: hir.Expr_Id) -> (i64, bool) {
current := expr_id
for current != hir.INVALID_EXPR && int(current) < len(module.exprs) {
expr := module.exprs[current]
if expr.kind == .Integer {
return expr.integer, true
}
if expr.kind != .Retype {
break
}
current = expr.left
}
return 0, false
}
build_globals :: proc(checker: ^Checker) {
for global, global_index in checker.ast_module.globals {
if global.diagnostic != source.INVALID_DIAGNOSTIC {
global_type := checker.global_types[global_index]
if !is_runtime_type(checker, global_type) {
global_type = types.I64
}
expr := hir.INVALID_EXPR
if !global.external {
expr = invalid_hir_expr(checker, global.span, global.diagnostic, global_type)
}
append(&checker.module.globals, hir.Global{
name=global.name,
link_name=strings.clone(global.link_name, checker.allocator),
type=global_type,
expr=expr,
external=global.external,
writable=global.writable || !global.immutable,
direct_problem=true,
problematic=true,
diagnostic=global.diagnostic,
})
continue
}
if global.external {
global_type := checker.global_types[global_index]
writable := global.writable
canonical := checker.external_global_canonical[global_index]
if canonical != ast.INVALID_GLOBAL && int(canonical) < len(checker.ast_module.globals) {
canonical_global := checker.ast_module.globals[canonical]
writable = canonical_global.writable
global_type = checker.global_types[canonical]
}
diagnostic := checker.external_global_diagnostics[global_index]
if !is_runtime_type(checker, global_type) {
if diagnostic == source.INVALID_DIAGNOSTIC {
diagnostic = source.addf(
checker.diagnostics,
global.span,
"could not resolve a concrete type for external global '%s'",
symbol_text(checker, global.name),
)
}
global_type = types.I64
}
_ = hir.global_id(len(checker.module.globals))
append(&checker.module.globals, hir.Global{
name=global.name,
link_name=strings.clone(global.link_name, checker.allocator),
type=global_type,
expr=hir.INVALID_EXPR,
external=true,
writable=writable,
direct_problem=diagnostic != source.INVALID_DIAGNOSTIC,
problematic=diagnostic != source.INVALID_DIAGNOSTIC,
diagnostic=diagnostic,
})
continue
}
dependencies: [dynamic]hir.Global_Id
dependencies.allocator = checker.allocator
calls: [dynamic]hir.Function_Id
calls.allocator = checker.allocator
declared := resolve_inferred_array(checker, type_from_syntax(checker, global.type, global.pkg, global.file), global.expr)
if has_inferred_array_count(checker, declared) &&
is_runtime_type(checker, checker.global_types[global_index]) {
declared = checker.global_types[global_index]
}
expected := types.INVALID
if is_runtime_type(checker, declared) {
expected = declared
} else if constant := eval_integer_constant_in_context(checker, global.expr, global.pkg, global.file);
constant.kind == .Value &&
(fits_i64(constant.value) || declared == types.UINT && fits_u64(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 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 := hir.INVALID_EXPR
if !global.immutable && is_undefined_expr(checker, global.expr) {
expr = add_hir_expr(checker, hir.Expr{
kind=.Undefined,
span=checker.ast_module.exprs[global.expr].span,
type=checker.global_types[global_index],
target=hir.INVALID_REF,
left=hir.INVALID_EXPR,
right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
} else {
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, checker.global_types[global_index]) {
expr = coerce_expr(checker, expr, checker.global_types[global_index], 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 root, invalid := invalid_expr_diagnostic(checker, expr); invalid {
diagnostic = root
if !is_runtime_type(checker, global_type) {
global_type = types.I64
}
}
if diagnostic == source.INVALID_DIAGNOSTIC && types.is_constraint(declared) &&
!types.constraint_accepts(declared, global_type, &checker.module.types) {
diagnostic = source.addf(
checker.diagnostics,
global.span,
"could not resolve the '%s' constraint for global '%s'",
types.name(declared),
symbol_text(checker, global.name),
)
global_type = constraint_recovery_type(checker, declared)
expr = invalid_hir_expr(checker, global.span, diagnostic, global_type)
}
if diagnostic == source.INVALID_DIAGNOSTIC && !is_runtime_type(checker, global_type) {
if types.is_comptime_only(global_type, &checker.module.types) ||
types.is_comptime_only(declared, &checker.module.types) {
diagnostic = source.addf(
checker.diagnostics,
global.span,
"global '%s' has a comptime-only type and cannot be stored at runtime",
symbol_text(checker, global.name),
)
} else {
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)
}
static_value, is_static := static_integer_value(&checker.module, expr)
is_static = is_static && diagnostic == source.INVALID_DIAGNOSTIC && global.immutable
_ = hir.global_id(len(checker.module.globals))
append(
&checker.module.globals,
hir.Global {
name = global.name,
link_name = strings.clone(global.link_name, checker.allocator),
type = global_type,
expr = expr,
static_value = static_value,
is_static = is_static,
external = false,
writable = !global.immutable,
dependencies = dependencies,
calls = calls[:],
direct_problem = expr_problematic(checker, expr),
problematic = expr_problematic(checker, expr),
diagnostic = diagnostic,
},
)
}
}
propagate_problems :: proc(checker: ^Checker) {
changed := true
for changed {
changed = false
for &function in checker.module.functions {
if function.problematic {
continue
}
for call in function.calls {
if call != hir.INVALID_FUNCTION && int(call) < len(checker.module.functions) &&
checker.module.functions[call].problematic {
function.problematic = true
changed = true
break
}
}
}
for &global in checker.module.globals {
if global.problematic {
continue
}
for dependency in global.dependencies {
if dependency != hir.INVALID_GLOBAL &&
int(dependency) < len(checker.module.globals) &&
checker.module.globals[dependency].problematic {
global.problematic = true
changed = true
break
}
}
if global.problematic {
continue
}
for call in global.calls {
if call != hir.INVALID_FUNCTION && int(call) < len(checker.module.functions) &&
checker.module.functions[call].problematic {
global.problematic = true
changed = true
break
}
}
}
}
}
append_unique_global :: proc(values: ^[dynamic]hir.Global_Id, value: hir.Global_Id) -> bool {
for existing in values^ {
if existing == value {
return false
}
}
append(values, value)
return true
}
propagate_global_reads :: proc(checker: ^Checker) {
changed := true
for changed {
changed = false
for &function in checker.module.functions {
for call in function.calls {
if call == hir.INVALID_FUNCTION || int(call) >= len(checker.module.functions) {
continue
}
for global_id in checker.module.functions[call].direct_global_reads {
if append_unique_global(&function.direct_global_reads, global_id) {
changed = true
}
}
}
}
}
for &global in checker.module.globals {
for call in global.calls {
if call == hir.INVALID_FUNCTION || int(call) >= len(checker.module.functions) {
continue
}
for dependency in checker.module.functions[call].direct_global_reads {
_ = append_unique_global(&global.dependencies, dependency)
}
}
}
}
Cycle_Frame :: struct {
global: hir.Global_Id,
next_dependency: int,
}
cycle_global_location :: proc(checker: ^Checker, global_id: hir.Global_Id) -> (name, path: string, line: int) {
name = "<unknown>"
path = "<unknown>"
line = 1
index := int(global_id)
if global_id == hir.INVALID_GLOBAL || index >= len(checker.module.globals) {
return
}
name = symbol_text(checker, checker.module.globals[global_id].name)
if index >= len(checker.ast_module.globals) {
return
}
span := checker.ast_module.globals[global_id].span
source_file := source.source_for_span(checker.diagnostics, span)
if source_file == nil {
return
}
path = source_file.path
line, _ = source.line_and_column(source_file, span.start)
return
}
detect_global_cycles_visit :: proc(checker: ^Checker, global_id: hir.Global_Id, states: []u8) {
if states[global_id] == 2 {
return
}
stack := checker.cycle_stack
clear_dynamic_array(&stack)
defer {
clear_dynamic_array(&stack)
checker.cycle_stack = stack
}
append(&stack, Cycle_Frame{global=global_id})
for len(stack) > 0 {
frame_index := len(stack)-1
frame := &stack[frame_index]
if states[frame.global] == 0 {
states[frame.global] = 1
}
dependencies := checker.module.globals[frame.global].dependencies
if frame.next_dependency >= len(dependencies) {
states[frame.global] = 2
if checker.module.globals[frame.global].problematic && frame_index > 0 {
checker.module.globals[stack[frame_index-1].global].problematic = true
}
_ = pop(&stack)
continue
}
dependency := dependencies[frame.next_dependency]
frame.next_dependency += 1
if dependency == hir.INVALID_GLOBAL || int(dependency) >= len(states) {
continue
}
if states[dependency] == 1 {
start_name, start_path, start_line := cycle_global_location(checker, dependency)
end_name, end_path, end_line := cycle_global_location(checker, frame.global)
id := source.addf(
checker.diagnostics,
checker.ast_module.globals[dependency].span,
"global initialization cycle from '%s' at %s:%d to '%s' at %s:%d",
start_name,
start_path,
start_line,
end_name,
end_path,
end_line,
)
checker.module.globals[dependency].diagnostic = id
checker.module.globals[dependency].problematic = true
checker.module.globals[frame.global].problematic = true
continue
}
if states[dependency] == 2 {
if checker.module.globals[dependency].problematic {
checker.module.globals[frame.global].problematic = true
}
continue
}
append(&stack, Cycle_Frame{global=dependency})
}
}
synthesize_trap_main :: proc(checker: ^Checker) {
id := source.add(checker.diagnostics, source.Span{}, "missing or unusable main function")
statement_id := hir.stmt_id(len(checker.module.statements))
_ = hir.function_id(len(checker.module.functions))
append(
&checker.module.statements,
hir.Stmt{kind = .Trap, span = source.Span{}, expr = hir.INVALID_EXPR, local = hir.INVALID_LOCAL, diagnostic = id},
)
body := make([]hir.Stmt_Id, 1, checker.allocator)
body[0] = statement_id
append(
&checker.module.functions,
hir.Function {
name = checker.main_symbol,
link_name = fmt.aprintf("main", allocator = checker.allocator),
calling_convention = .C,
implementation = .Definition,
linkage = .External,
is_main = true,
result = types.VOID,
body = body,
problematic = true,
diagnostic = id,
},
)
}
replace_main_with_trap :: proc(checker: ^Checker, diagnostic: source.Diagnostic_Id) {
for &function in checker.module.functions {
if !function.is_main {
continue
}
delete(function.params, checker.allocator)
delete(function.body, checker.allocator)
function.params = nil
function.result = types.VOID
function.calling_convention = .C
function.implementation = .Definition
function.linkage = .External
function.problematic = true
function.diagnostic = diagnostic
statement_id := hir.stmt_id(len(checker.module.statements))
append(
&checker.module.statements,
hir.Stmt {
kind = .Trap,
span = source.Span{},
expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL,
diagnostic = diagnostic,
},
)
function.body = make([]hir.Stmt_Id, 1, checker.allocator)
function.body[0] = statement_id
return
}
synthesize_trap_main(checker)
}
check :: proc(
ast_module: ^ast.Module,
diagnostics: ^source.Diagnostics,
symbols: ^symbol.Table,
selected := target.DEFAULT,
allocator := context.allocator,
) -> hir.Module {
checker := Checker {
ast_module = ast_module,
diagnostics = diagnostics,
symbols = symbols,
module = hir.init_module(selected, allocator),
main_symbol = symbol.intern(symbols, "main"),
io_provider_template = ast.INVALID_FUNCTION,
sink_symbol = symbol.intern(symbols, "_"),
type_symbol = symbol.intern(symbols, "type"),
target = selected,
allocator = allocator,
}
checker.specs.allocator = allocator
types.destroy_store(&checker.module.types)
checker.module.types = types.clone_store(&ast_module.type_store, allocator)
checker.module.types.selected = selected
for value in ast_module.strings {
append(&checker.module.strings, strings.clone(value, allocator))
}
checker.constant_stack.allocator = allocator
checker.hir_expr_stack.allocator = allocator
checker.infer_stack.allocator = allocator
checker.build_stack.allocator = allocator
checker.cycle_stack.allocator = allocator
checker.anon_globals.allocator = allocator
checker.type_factories.allocator = allocator
checker.generated_types.allocator = allocator
checker.type_factory_origins.allocator = allocator
checker.call_resolutions.allocator = allocator
checker.static_bindings.allocator = allocator
checker.comptime_keys.allocator = allocator
checker.comptime_static_values.allocator = allocator
checker.expand_context.allocator = allocator
checker.static_state = ct_state_make(&checker, 0, ast.INVALID_FILE)
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.record_field_constraints = make([]types.Type, len(checker.module.types.fields), allocator)
checker.record_field_defaults = make([]types.Type, len(checker.module.types.fields), allocator)
checker.record_field_conflicts = make([]types.Type, len(checker.module.types.fields), allocator)
checker.record_field_conflict_spans = make([]source.Span, len(checker.module.types.fields), allocator)
checker.poisoned_packages = make([]bool, max(len(ast_module.packages), 1), allocator)
init_record_field_inference(&checker)
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, index in checker.template_diagnostics {
diagnostic = ast_module.functions[index].diagnostic
pkg := ast_module.functions[index].pkg
if diagnostic != source.INVALID_DIAGNOSTIC && int(pkg) < len(checker.poisoned_packages) {
checker.poisoned_packages[pkg] = true
}
}
for global in ast_module.globals {
if global.diagnostic != source.INVALID_DIAGNOSTIC && int(global.pkg) < len(checker.poisoned_packages) {
checker.poisoned_packages[global.pkg] = true
}
}
defer {
for spec in checker.specs {
delete(spec.args, allocator)
delete(spec.comptime_values, allocator)
}
delete(checker.specs)
delete(checker.function_index, allocator)
delete(checker.global_index, allocator)
delete(checker.import_index, allocator)
delete(checker.global_types, allocator)
delete(checker.global_demands, allocator)
delete(checker.global_open_const, allocator)
delete(checker.global_open_float, allocator)
delete(checker.global_const_value, allocator)
delete(checker.record_field_constraints, allocator)
delete(checker.record_field_defaults, allocator)
delete(checker.record_field_conflicts, allocator)
delete(checker.record_field_conflict_spans, allocator)
delete(checker.poisoned_packages, 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.hir_expr_stack)
delete(checker.infer_stack)
delete(checker.build_stack)
delete(checker.cycle_stack)
for entry in checker.type_factories {
delete(entry.values, allocator)
}
for entry in checker.generated_types {
delete(entry.values, allocator)
delete(entry.defaults, allocator)
}
for origin in checker.type_factory_origins {
delete(origin.values, allocator)
}
for resolution in checker.call_resolutions {
delete(resolution.ctx, allocator)
delete(resolution.expand_ctx, allocator)
delete(resolution.mapping, allocator)
delete(resolution.comptime_values, allocator)
delete(resolution.runtime_types, allocator)
}
delete(checker.type_factories)
delete(checker.generated_types)
delete(checker.type_factory_origins)
delete(checker.call_resolutions)
ct_state_destroy(&checker.static_state)
delete(checker.static_bindings)
for key in checker.comptime_keys {
delete(key, allocator)
}
delete(checker.comptime_keys)
delete(checker.comptime_static_values)
delete(checker.expand_context)
delete(checker.anon_globals)
}
for function, index in ast_module.functions {
if function.generated {
continue
}
for previous in ast_module.functions[:index] {
if !previous.generated && previous.pkg == function.pkg && previous.name == function.name {
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))
}
}
if type_declaration_conflicts(&checker, function.pkg, function.name) {
source.addf(diagnostics, function.span, "function '%s' shadows visible type", symbol_text(&checker, function.name))
}
}
for global, index in ast_module.globals {
for previous in ast_module.globals[:index] {
if previous.pkg == global.pkg && previous.name == global.name {
source.addf(diagnostics, global.span, "duplicate global '%s'", symbol_text(&checker, global.name))
}
}
if type_declaration_conflicts(&checker, global.pkg, global.name) {
source.addf(diagnostics, global.span, "global '%s' shadows visible type", symbol_text(&checker, global.name))
}
}
validate_type_nodes(&checker)
validate_declarations(&checker)
configure_entry_point(&checker)
infer_all(&checker)
finalize_record_field_inference(&checker)
validate_external_globals(&checker)
prune_specs(&checker)
checker.building_hir = true
build_globals(&checker)
for index := 0; index < len(checker.specs); index += 1 {
build_function(&checker, spec_id(index))
}
// Flush anonymous globals synthesized for `&<array literal>`. Appended only now
// (after every ast global was built at its identity-mapped index) so their ids,
// pre-assigned as len(ast.globals)+stage_index, land exactly.
for anon in checker.anon_globals {
append(&checker.module.globals, anon)
}
propagate_global_reads(&checker)
main_template := find_template(&checker, checker.main_symbol, 0)
main_declarations := 0
for function in ast_module.functions {
if !function.generated && 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]
valid_params := checker.entry_point == .Plain && len(template.params) == 0 ||
checker.entry_point == .Process && len(template.params) == 1
if main_declarations != 1 ||
!template.has_body ||
!valid_params ||
!(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 or one @std/process Init, and return void, i32, or int, optionally with an error channel",
)
}
checker.module.injected_main = hir.INVALID_FUNCTION
checker.module.io_provider = hir.INVALID_FUNCTION
replace_main_with_trap(&checker, id)
} else {
main_spec := find_spec(&checker, main_template, nil)
if checker.entry_point == .Process {
provider_spec := find_spec(&checker, checker.io_provider_template, nil)
if main_spec != INVALID_SPEC && provider_spec != INVALID_SPEC {
checker.module.injected_main = checker.specs[main_spec].hir_id
checker.module.io_provider = checker.specs[provider_spec].hir_id
}
} else if main_spec != INVALID_SPEC &&
types.kind(checker.specs[main_spec].result, &checker.module.types) == .Fallible {
checker.module.injected_main = checker.specs[main_spec].hir_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.test_only && import_item.valid && !import_item.used {
source.addf_warning(diagnostics, import_item.span, "unused import '%s'", symbol_text(&checker, import_item.alias))
}
}
return checker.module
}