runtime mutable global state

This commit is contained in:
2026-07-03 21:48:23 +02:00
parent adf142736c
commit ee41a54e41
11 changed files with 569 additions and 40 deletions
+337 -15
View File
@@ -534,6 +534,90 @@ find_import :: proc(checker: ^Checker, file: ast.File_Id, alias: symbol.Id, mark
return id
}
declared_type_named :: proc(checker: ^Checker, pkg: ast.Package_Id, name: symbol.Id) -> bool {
id := types.find_named(&checker.module.types, u32(pkg), u32(name))
item, ok := types.node(&checker.module.types, id)
return ok && item.declared
}
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) != ast.INVALID_GLOBAL {
return "global"
}
if find_template(checker, name, pkg) != ast.INVALID_FUNCTION {
return "function"
}
if declared_type_named(checker, pkg, name) {
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
}
return add_shadow_diagnostic(
ctx.checker, span, label, "label",
ctx.pkg, ctx.file, ctx.locals^[:], ctx.loop_labels^[:], ctx.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
@@ -1081,6 +1165,11 @@ validate_declarations :: proc(checker: ^Checker) {
"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 && types.contains_c_struct_by_value(param_type, &checker.module.types) {
@@ -1748,6 +1837,31 @@ infer_expr :: proc(
}
}
}
if !types.is_valid(last) && symbol.is_valid(expr.qualifier) &&
find_import(checker, file, expr.qualifier) == ast.INVALID_IMPORT {
if global := find_global(checker, expr.qualifier, pkg); 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) {
_, field, ok := find_struct_field(checker, base_type, expr.name)
if ok {
last = field.type
}
}
}
}
if !types.is_valid(last) {
if !symbol.is_valid(expr.qualifier) {
if value, ok := current_comptime_value(checker, expr.name); ok {
@@ -2200,6 +2314,11 @@ infer_statements :: proc(
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); 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:
@@ -2462,6 +2581,11 @@ merge_global_demand :: proc(checker: ^Checker, global: ast.Global_Id, demand: ty
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(
@@ -2766,6 +2890,20 @@ infer_all :: proc(checker: ^Checker) {
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).
@@ -4229,6 +4367,43 @@ build_expr :: proc(
})
}
}
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); 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 value, ok := current_comptime_value(checker, expr.name); ok {
@@ -4839,6 +5014,18 @@ build_block :: proc(
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 := hir.local_id(len(ctx.hir_locals^))
append(ctx.hir_locals, hir.Local{
name = statement.name, type = value_type, mutable = !statement.immutable,
@@ -4954,6 +5141,18 @@ build_block :: proc(
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 := hir.local_id(len(ctx.hir_locals^))
append(ctx.hir_locals, hir.Local{
name = statement.name, type = value_type, mutable = !statement.immutable,
@@ -5094,13 +5293,46 @@ build_block :: proc(
}
local, found := find_build_local(ctx.locals^[:], statement.name)
if !found {
id := source.addf(checker.diagnostics, statement.span, "cannot assign unresolved local '%s'", symbol_text(checker, statement.name))
global := find_global(checker, statement.name, ctx.pkg)
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)
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 = .Trap, span = statement.span, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = id,
kind=.Assignment, span=statement.span, local=hir.INVALID_LOCAL,
assignment_op=.Set,
target=target_expr, expr=value, diagnostic=source.INVALID_DIAGNOSTIC,
})
ctx.problematic^ = true
ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid
continue
}
if !local.mutable {
@@ -5343,6 +5575,12 @@ build_block :: proc(
"'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 = hir.local_id(len(ctx.hir_locals^))
append(ctx.hir_locals, hir.Local{name=capture, type=child, mutable=false})
@@ -5445,6 +5683,15 @@ build_block :: proc(
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)
@@ -5530,6 +5777,15 @@ build_block :: proc(
}
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 := hir.local_id(len(ctx.hir_locals^))
append(ctx.hir_locals, hir.Local{name=statement.name, type=capture_type, mutable=false})
append(ctx.locals, Build_Local{name=statement.name, type=capture_type, mutable=false, id=item_local})
@@ -5538,12 +5794,27 @@ build_block :: proc(
if statement.index_name == statement.name {
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 = hir.local_id(len(ctx.hir_locals^))
append(ctx.hir_locals, hir.Local{name=statement.index_name, type=types.USIZE, mutable=false})
append(ctx.locals, Build_Local{name=statement.index_name, type=types.USIZE, mutable=false, id=index_local})
}
}
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)
@@ -5636,6 +5907,15 @@ build_block :: proc(
// 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)
@@ -6061,6 +6341,11 @@ emit_value_if :: proc(
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 = hir.local_id(len(ctx.hir_locals^))
append(ctx.hir_locals, hir.Local{name=capture, type=child, mutable=false})
@@ -6594,10 +6879,22 @@ build_match_arm_body :: proc(
result: [dynamic]hir.Stmt_Id
result.allocator = checker.allocator
capture_start := len(ctx.locals^)
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.
@@ -6626,7 +6923,7 @@ build_match_arm_body :: proc(
}
}
body_ok := true
body_ok := capture_ok
if !as_value {
built := build_block(ctx, arm.body)
for s in built {
@@ -6634,7 +6931,7 @@ build_match_arm_body :: proc(
}
delete(built, checker.allocator)
} else {
body_ok = build_value_arm(ctx, &result, arm.body, slot, slot_type, span)
body_ok = build_value_arm(ctx, &result, arm.body, slot, slot_type, span) && body_ok
}
resize(ctx.locals, capture_start)
return result[:], body_ok
@@ -6911,6 +7208,10 @@ build_value_labeled_block :: proc(
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^),
@@ -7438,7 +7739,18 @@ build_globals :: proc(checker: ^Checker) {
global_type = checker.module.exprs[expr].type
}
diagnostic := source.INVALID_DIAGNOSTIC
if !is_runtime_type(checker, global_type) {
if !global.immutable && is_undefined_expr(checker, global.expr) {
diagnostic = source.add(
checker.diagnostics,
global.span,
"'undefined' is only valid as a mutable local declaration initializer",
)
if !is_runtime_type(checker, global_type) {
global_type = types.I64
}
expr = invalid_hir_expr(checker, global.span, diagnostic, global_type)
}
if diagnostic == source.INVALID_DIAGNOSTIC && !is_runtime_type(checker, global_type) {
diagnostic = source.addf(
checker.diagnostics,
global.span,
@@ -7457,15 +7769,19 @@ build_globals :: proc(checker: ^Checker) {
expr = invalid_hir_expr(checker, global.span, diagnostic)
}
if !global.immutable {
diagnostic = source.add(
checker.diagnostics,
global.span,
"mutable declarations are only valid inside functions",
)
expr = invalid_hir_expr(checker, global.span, diagnostic)
if !types.is_valid(global.type) {
diagnostic = source.addf(
checker.diagnostics,
global.span,
"mutable global '%s' requires a type annotation",
symbol_text(checker, global.name),
)
global_type = types.I64
expr = invalid_hir_expr(checker, global.span, diagnostic, global_type)
}
}
static_value, is_static := static_integer_value(&checker.module, expr)
is_static = is_static && diagnostic == source.INVALID_DIAGNOSTIC
is_static = is_static && diagnostic == source.INVALID_DIAGNOSTIC && global.immutable
_ = hir.global_id(len(checker.module.globals))
append(
&checker.module.globals,
@@ -7477,7 +7793,7 @@ build_globals :: proc(checker: ^Checker) {
static_value = static_value,
is_static = is_static,
external = false,
writable = false,
writable = !global.immutable,
dependencies = dependencies,
calls = calls[:],
direct_problem = expr_problematic(checker, expr),
@@ -7805,6 +8121,9 @@ check :: proc(
source.addf(diagnostics, function.span, "package declaration '%s' conflicts with a global", symbol_text(&checker, function.name))
}
}
if declared_type_named(&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] {
@@ -7812,6 +8131,9 @@ check :: proc(
source.addf(diagnostics, global.span, "duplicate global '%s'", symbol_text(&checker, global.name))
}
}
if declared_type_named(&checker, global.pkg, global.name) {
source.addf(diagnostics, global.span, "global '%s' shadows visible type", symbol_text(&checker, global.name))
}
}
validate_type_nodes(&checker)