undefined assignment

This commit is contained in:
2026-06-25 20:37:20 +02:00
parent e0b6f6049c
commit ca6cc89da0
10 changed files with 489 additions and 47 deletions
+226 -44
View File
@@ -33,8 +33,11 @@ Spec :: struct {
}
Infer_Local :: struct {
name: symbol.Id,
type: types.Type,
name: symbol.Id,
type: types.Type,
declared: types.Type,
statement: ast.Stmt_Id,
mutable: bool,
}
Build_Local :: struct {
@@ -53,6 +56,7 @@ Build_Ctx :: struct {
pkg: ast.Package_Id,
file: ast.File_Id,
result: types.Type,
local_types: []types.Type,
locals: ^[dynamic]Build_Local,
hir_locals: ^[dynamic]hir.Local,
global_reads: ^[dynamic]hir.Global_Id,
@@ -262,6 +266,13 @@ is_runtime_type :: proc(checker: ^Checker, value: types.Type) -> bool {
return types.is_runtime_value(value, &checker.module.types)
}
is_undefined_expr :: proc(checker: ^Checker, expr_id: ast.Expr_Id) -> bool {
if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) {
return false
}
return checker.ast_module.exprs[expr_id].kind == .Undefined
}
string_literal_type :: proc(checker: ^Checker, string_id: u64) -> types.Type {
length: u64
if string_id < u64(len(checker.ast_module.strings)) {
@@ -636,7 +647,7 @@ mark_expr_imports_used :: proc(checker: ^Checker, expr_id: ast.Expr_Id, file: as
append(&stack, expr.left)
case .Add, .Sub, .Mul, .Div, .Index, .Orelse, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range:
append(&stack, expr.left, expr.right)
case .Invalid, .Integer, .Float, .String, .Bool, .None, .Name, .Enum_Literal:
case .Invalid, .Integer, .Float, .String, .Bool, .None, .Undefined, .Name, .Enum_Literal:
}
}
}
@@ -956,14 +967,21 @@ validate_type_nodes :: proc(checker: ^Checker) {
}
find_infer_local :: proc(locals: []Infer_Local, name: symbol.Id) -> types.Type {
for index := len(locals) - 1; index >= 0; index -= 1 {
if locals[index].name == name {
return locals[index].type
}
if index, ok := find_infer_local_index(locals, name); ok {
return locals[index].type
}
return types.INVALID
}
find_infer_local_index :: proc(locals: []Infer_Local, name: symbol.Id) -> (int, bool) {
for index := len(locals) - 1; index >= 0; index -= 1 {
if locals[index].name == name {
return index, true
}
}
return -1, false
}
find_spec :: proc(checker: ^Checker, template: ast.Function_Id, actual_args: []types.Type) -> Spec_Id {
function := checker.ast_module.functions[template]
for spec, index in checker.specs {
@@ -1123,6 +1141,8 @@ infer_compound_expr :: proc(
return types.array(store, element, u64(len(expr.args)), false)
case .None:
return types.INVALID
case .Undefined:
return types.INVALID
case .Enum_Literal:
return types.INVALID
case .Address:
@@ -1247,7 +1267,7 @@ infer_expr :: proc(
case .Float:
last = types.F64
_ = pop(&stack)
case .String, .Array, .None, .Address, .Deref, .Index, .Slice,
case .String, .Array, .None, .Undefined, .Address, .Deref, .Index, .Slice,
.Field, .Unwrap, .Orelse, .Struct_Literal, .Keyed, .Enum_Literal,
.Bool, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range:
last = infer_compound_expr(checker, expr, locals, pkg, file, demanded)
@@ -1505,34 +1525,127 @@ flatten_conditional_unwrap_operands :: proc(
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.is_concrete_integer(inferred) {
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_integer(merged) {
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:
value_type := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded)
declared_local := type_from_syntax(statement.type)
declared_local := resolve_inferred_array(checker, type_from_syntax(statement.type), statement.expr)
value_type := types.INVALID
if !is_undefined_expr(checker, statement.expr) {
value_type = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded)
}
if is_runtime_type(checker, declared_local) {
value_type = declared_local
}
append(locals, Infer_Local{name = statement.name, type = value_type})
case .Assignment, .Expression:
local := Infer_Local{
name=statement.name,
type=value_type,
declared=declared_local,
statement=statement_id,
mutable=!statement.immutable,
}
append(locals, local)
record_infer_local_type(local, local_types)
case .Assignment:
value_type := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded)
if statement.target != ast.INVALID_EXPR {
_ = infer_expr(checker, statement.target, locals^[:], pkg, file, demanded)
target_expr := checker.ast_module.exprs[statement.target]
if target_expr.kind == .Name && !symbol.is_valid(target_expr.qualifier) {
if local_index, ok := find_infer_local_index(locals^[:], target_expr.name); ok &&
locals^[local_index].mutable {
_ = merge_infer_local_type(checker, &locals^[local_index], value_type, local_types)
}
}
} else if statement.name != checker.sink_symbol {
if local_index, ok := find_infer_local_index(locals^[:], statement.name); ok &&
locals^[local_index].mutable {
_ = merge_infer_local_type(checker, &locals^[local_index], value_type, local_types)
}
}
case .Expression:
_ = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded)
case .Return:
if statement.expr != ast.INVALID_EXPR {
returned := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded)
if is_runtime_type(checker, result_hint) {
expr := checker.ast_module.exprs[statement.expr]
if expr.kind == .Name && !symbol.is_valid(expr.qualifier) {
if local_index, ok := find_infer_local_index(locals^[:], expr.name); ok {
_ = merge_infer_local_type(checker, &locals^[local_index], result_hint, local_types)
returned = result_hint
}
}
}
if !types.is_valid(result^) {
result^ = returned
} else if !types.equal(result^, returned) {
@@ -1558,27 +1671,27 @@ infer_statements :: proc(
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})
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)
}
infer_statements(checker, statement.body, locals, pkg, file, demanded, result)
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, pkg, file, demanded, result)
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)
infer_statements(checker, statement.body, locals, pkg, file, demanded, result)
infer_statements(checker, statement.else_body, locals, pkg, file, demanded, result)
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)
infer_statements(checker, statement.body, locals, pkg, file, demanded, result)
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, pkg, file, demanded, result)
infer_statements(checker, update[:], locals, local_types, pkg, file, demanded, result, result_hint)
}
case .For:
iterable_type := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded)
@@ -1596,43 +1709,55 @@ infer_statements :: proc(
}
}
if symbol.is_valid(statement.name) {
append(locals, Infer_Local{name=statement.name, type=capture_type})
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})
append(locals, Infer_Local{name=statement.index_name, type=types.USIZE, declared=types.USIZE, statement=ast.INVALID_STMT})
}
infer_statements(checker, statement.body, locals, pkg, file, demanded, result)
infer_statements(checker, statement.body, locals, local_types, pkg, file, demanded, result, result_hint)
resize(locals, capture_start)
}
}
resize(locals, scope_start)
}
infer_spec_result :: proc(checker: ^Checker, id: Spec_Id, demanded: ^[dynamic]Spec_Id = nil) -> types.Type {
infer_spec_locals_and_result :: proc(
checker: ^Checker,
id: Spec_Id,
demanded: ^[dynamic]Spec_Id = nil,
) -> ([]types.Type, types.Type) {
spec := checker.specs[id]
function := checker.ast_module.functions[spec.template]
declared := type_from_syntax(function.result)
if function.pkg == 0 && function.name == checker.main_symbol && function.result == types.INT {
declared = types.I32
}
result_hint := declared if is_runtime_type(checker, declared) else types.INVALID
locals: [dynamic]Infer_Local
locals.allocator = checker.allocator
defer delete(locals)
local_types := make([]types.Type, len(checker.ast_module.statements), checker.allocator)
for param, index in function.params {
param_type := types.INVALID
if index < len(spec.args) {
param_type = spec.args[index]
}
append(&locals, Infer_Local{name = param.name, type = param_type})
append(&locals, Infer_Local{name=param.name, type=param_type, declared=param_type, statement=ast.INVALID_STMT})
}
result := types.INVALID
infer_statements(checker, function.body, &locals, function.pkg, function.file, demanded, &result)
infer_statements(checker, function.body, &locals, local_types, function.pkg, function.file, demanded, &result, result_hint)
if types.is_constraint(declared) {
return result
return local_types, result
}
return declared
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 {
@@ -2296,7 +2421,7 @@ build_compound_expr :: proc(
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}
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)
@@ -2332,6 +2457,13 @@ build_compound_expr :: proc(
kind=.None, span=expr.span, type=expected, target=hir.INVALID_REF,
left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC,
})
case .Undefined:
id := source.add(
checker.diagnostics,
expr.span,
"'undefined' is only valid as a mutable local declaration initializer",
)
return invalid_hir_expr(checker, expr.span, id, expected)
case .Enum_Literal:
if !types.is_enum(expected, store) {
id := source.addf(
@@ -2762,7 +2894,7 @@ build_expr :: proc(
continue
}
switch expr.kind {
case .String, .Array, .None, .Address, .Deref, .Index, .Slice,
case .String, .Array, .None, .Undefined, .Address, .Deref, .Index, .Slice,
.Field, .Unwrap, .Orelse, .Struct_Literal, .Keyed,
.Bool, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range,
.Enum_Literal:
@@ -3290,22 +3422,62 @@ build_block :: proc(
switch statement.kind {
case .Declaration:
declared := resolve_inferred_array(checker, type_from_syntax(statement.type), statement.expr)
expected := types.INVALID
if is_runtime_type(checker, declared) {
expected = declared
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)) {
declared = ctx.local_types[statement_id]
}
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)
expected := types.INVALID
value := hir.INVALID_EXPR
value_type := types.INVALID
if is_undefined_expr(checker, statement.expr) {
if statement.immutable {
id := source.add(
checker.diagnostics,
statement.span,
"'undefined' requires a mutable local declaration",
)
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = id,
})
ctx.problematic^ = true
continue
}
if !is_runtime_type(checker, declared) {
id := source.addf(
checker.diagnostics,
statement.span,
"could not infer a concrete type for local '%s'",
symbol_text(checker, statement.name),
)
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = id,
})
ctx.problematic^ = true
continue
}
value_type = declared
} else {
if is_runtime_type(checker, declared) {
expected = declared
}
value = build_expr(
checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls,
expected, ctx.pkg, ctx.file,
)
value_type = checker.module.exprs[value].type
} 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 is_runtime_type(checker, declared) {
value = coerce_expr(checker, value, declared, statement.span)
value_type = checker.module.exprs[value].type
} else if types.is_void(declared) {
id := source.add(checker.diagnostics, statement.span, "locals cannot have type void")
value = invalid_hir_expr(checker, statement.span, id)
value_type = types.INVALID
}
}
if _, found := find_build_local(ctx.locals^[duplicate_start:], statement.name); found {
id := source.addf(
@@ -3332,7 +3504,9 @@ build_block :: proc(
kind = .Declaration, span = statement.span, local = local_id, expr = value,
diagnostic = source.INVALID_DIAGNOSTIC,
})
ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid
if value != hir.INVALID_EXPR {
ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid
}
case .Assignment:
if statement.target != ast.INVALID_EXPR {
target_expr := build_expr(
@@ -3888,6 +4062,13 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) {
global_reads.allocator = checker.allocator
calls: [dynamic]hir.Function_Id
calls.allocator = checker.allocator
demanded: [dynamic]Spec_Id
demanded.allocator = checker.allocator
local_types, _ := infer_spec_locals_and_result(checker, id, &demanded)
defer {
delete(local_types, checker.allocator)
delete(demanded)
}
for param, index in function.params {
local_id := hir.local_id(len(hir_locals))
@@ -3946,6 +4127,7 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) {
pkg = function.pkg,
file = function.file,
result = spec.result,
local_types = local_types,
locals = &locals,
hir_locals = &hir_locals,
global_reads = &global_reads,