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
+3 -3
View File
@@ -8,8 +8,8 @@ roadmap and milestone history.
### source, declarations, and packages ### source, declarations, and packages
- newline-terminated statements and `#` comments - newline-terminated statements and `#` comments
- immutable `::` bindings, mutable function-local `=` bindings, and `_` sinks - immutable `::` bindings, typed mutable `=` locals/globals, and `_` sinks
- immutable package globals, function-local mutable locals, and mutable local declarations initialized with `undefined` - immutable package globals, mutable runtime globals, function-local mutable locals, and mutable local declarations initialized with `undefined`
- package-level functions, globals, native type declarations, and `Name :: alias T` - package-level functions, globals, native type declarations, and `Name :: alias T`
- directory packages with merged declarations - directory packages with merged declarations
- file-local relative imports, import aliases, and qualified member access - file-local relative imports, import aliases, and qualified member access
@@ -76,7 +76,7 @@ roadmap and milestone history.
- error-tolerant compilation with diagnostics and runtime traps where recovery is possible - error-tolerant compilation with diagnostics and runtime traps where recovery is possible
- lazy semantic checking of demanded function specializations - lazy semantic checking of demanded function specializations
- static, eager runtime, and deferred problematic globals with cycle diagnostics - static, eager runtime, mutable runtime, and deferred problematic globals with cycle diagnostics
- demand-driven LLVM declarations for referenced foreign functions - demand-driven LLVM declarations for referenced foreign functions
- replaceable dynamically loaded libclang C-import backend - replaceable dynamically loaded libclang C-import backend
- C-header import caching by canonical path, target, include paths, and defines - C-header import caching by canonical path, target, include paths, and defines
+2 -2
View File
@@ -124,7 +124,7 @@ Current prototype features:
- Newline-terminated, multiline statements; `}` may terminate a block's final statement - Newline-terminated, multiline statements; `}` may terminate a block's final statement
- `#` comments - `#` comments
- Immutable `::` bindings, mutable function-local `=` bindings, and `_` sinks - Immutable `::` bindings, typed mutable `=` locals/globals, and `_` sinks
- Exact-width signed/unsigned integers, `f32`, `f64`, `isize`, `usize`, and loose integer-constrained `int` - Exact-width signed/unsigned integers, `f32`, `f64`, `isize`, `usize`, and loose integer-constrained `int`
- Target-dependent atomic `c_*` primitive types, `c_func`, and defined or opaque `c_struct` - Target-dependent atomic `c_*` primitive types, `c_func`, and defined or opaque `c_struct`
- Arrays, sentinel arrays, single-item pointers, many-item pointers, sentinel many-item pointers, slices, sentinel slices, strings, character literals, optionals, and native structs - Arrays, sentinel arrays, single-item pointers, many-item pointers, sentinel many-item pointers, slices, sentinel slices, strings, character literals, optionals, and native structs
@@ -143,7 +143,7 @@ Current prototype features:
- Bodyless manual and imported C variadic declarations with default argument promotions - Bodyless manual and imported C variadic declarations with default argument promotions
- Ordered linking of additional C sources, objects, archives, and libraries - Ordered linking of additional C sources, objects, archives, and libraries
- Checked signed addition and unary negation - Checked signed addition and unary negation
- Static, eager runtime, and deferred problematic globals - Static, eager runtime, mutable runtime, and deferred problematic globals
- Runtime diagnostics followed by `llvm.trap` - Runtime diagnostics followed by `llvm.trap`
See [LANGUAGE.md](LANGUAGE.md) for the concise implemented and planned language See [LANGUAGE.md](LANGUAGE.md) for the concise implemented and planned language
+28 -3
View File
@@ -91,7 +91,7 @@
- operators: `and`, `or`, `!` - operators: `and`, `or`, `!`
- lazy evaluation / short-circuit evaluation - lazy evaluation / short-circuit evaluation
- if statements (implemented). example: `if condition { ... } else if { ... } else { ... }` - if statements (implemented). example: `if condition { ... } else if { ... } else { ... }`
- conditions must be `bool`; block-scoped locals with shadowing across blocks - conditions must be `bool`; block-scoped locals do not escape their blocks
- lowered through new `Label` / `Br` / `Cond_Br` IR opcodes (alloca-backed locals, no phi nodes) - lowered through new `Label` / `Br` / `Cond_Br` IR opcodes (alloca-backed locals, no phi nodes)
- conditional unwrapping for optionals (`?T`) (implemented): `if val |v| { ... } else { ... }` - unwrap `val` into `v` if it is not `none` - conditional unwrapping for optionals (`?T`) (implemented): `if val |v| { ... } else { ... }` - unwrap `val` into `v` if it is not `none`
- single immutable binding scoped to the then-block; `v` not visible in `else` or after the `if` - single immutable binding scoped to the then-block; `v` not visible in `else` or after the `if`
@@ -403,8 +403,7 @@
statements first (a throwaway probe), so a first concrete `yield :blk` that references a statements first (a throwaway probe), so a first concrete `yield :blk` that references a
block local still resolves the result to `?T` block local still resolves the result to `?T`
- deferred (`// ponytail:`): the same `none`-before-concrete typing in an untyped block (or - deferred (`// ponytail:`): the same `none`-before-concrete typing in an untyped block (or
loop) whose concrete yield references a local declared *past* the first yield (annotate); loop) whose concrete yield references a local declared *past* the first yield (annotate)
same-label loop/block shadowing resolves innermost-wins
21. unions and tagged unions (implemented; first pass — native untagged unions only; see below) 21. unions and tagged unions (implemented; first pass — native untagged unions only; see below)
- inspired by zig - inspired by zig
@@ -683,6 +682,32 @@
stable aggregate serialization, comptime pointers/slices, and calls through stable aggregate serialization, comptime pointers/slices, and calls through
comptime-known function values/function pointers are deferred comptime-known function values/function pointers are deferred
27.8 source-defined mutable runtime globals (implemented)
- allow mutable global declarations in Brolang source for process-global runtime
state, matching the writable-global support already needed for imported C globals
- require source type syntax and an initializer; constraints (`int`/`float`/`range`)
and inferred array counts may resolve through the existing inference fixpoint, but
the final type must be concrete runtime storage
- emit source-defined mutable globals as writable globals, not constants
- allow assignment, address-taking, field/index mutation, and pointer passing under
the same mutability rules as other writable locations
- keep mutable globals invalid in comptime evaluation; `$global_var` and writes from
comptime execution must remain runtime-dependent errors
- define initialization order and cycle behavior by reusing the existing global
initializer dependency/cycle system where possible
- reject user-visible name shadowing across imports, named types, globals, functions,
params, locals, comptime params, captures, and labels; `_` remains reusable
27.9 comptime storage and function values
- add a comptime pointer/storage model for pointers, slices, address/deref,
pointer captures, lifetimes, aliasing, mutability, and escape rules
- define what `$&value` and other comptime addresses can legally materialize into,
without exposing compiler-owned memory as runtime memory
- add first-class comptime function values and calls through comptime-known function
pointers
- resolve function-pointer targets during comptime execution, apply ABI/runtime
restrictions, and reliably reject imported/runtime callbacks
28. brolang build system (requires comptime execution) 28. brolang build system (requires comptime execution)
## A word on multi-unwrap ## A word on multi-unwrap
+337 -15
View File
@@ -534,6 +534,90 @@ find_import :: proc(checker: ^Checker, file: ast.File_Id, alias: symbol.Id, mark
return id 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) { 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) { if !symbol.is_valid(expr.qualifier) {
return pkg, true return pkg, true
@@ -1081,6 +1165,11 @@ validate_declarations :: proc(checker: ^Checker) {
"duplicate parameter '%s'", "duplicate parameter '%s'",
symbol_text(checker, param.name), symbol_text(checker, param.name),
) )
} else {
_ = add_shadow_diagnostic(
checker, param.span, param.name, "parameter",
function.pkg, function.file,
)
} }
append(&locals, param.name) append(&locals, param.name)
if !has_comptime && types.contains_c_struct_by_value(param_type, &checker.module.types) { 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 !types.is_valid(last) {
if !symbol.is_valid(expr.qualifier) { if !symbol.is_valid(expr.qualifier) {
if value, ok := current_comptime_value(checker, expr.name); ok { if value, ok := current_comptime_value(checker, expr.name); ok {
@@ -2200,6 +2314,11 @@ infer_statements :: proc(
if !rhs_is_arith { if !rhs_is_arith {
_ = record_demand(checker, statement.expr, locals^[local_index].type, locals^[:], local_types, pkg, file) _ = 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: 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) { if index < 0 || index >= len(checker.global_demands) {
return false 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 changed: bool
if checker.global_open_const[index] || checker.global_open_float[index] { if checker.global_open_const[index] || checker.global_open_float[index] {
changed = merge_open_const_demand( 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) { if is_runtime_type(checker, declared) && !has_inferred_array_count(checker, declared) {
continue 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]) { if is_runtime_type(checker, checker.global_demands[index]) {
// A backward demand is authoritative; assign directly (it may cross the // A backward demand is authoritative; assign directly (it may cross the
// signed/unsigned family that widening would reject). // 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 last == hir.INVALID_EXPR {
if !symbol.is_valid(expr.qualifier) { if !symbol.is_valid(expr.qualifier) {
if value, ok := current_comptime_value(checker, expr.name); ok { if value, ok := current_comptime_value(checker, expr.name); ok {
@@ -4839,6 +5014,18 @@ build_block :: proc(
ctx.problematic^ = true ctx.problematic^ = true
continue 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^)) local_id := hir.local_id(len(ctx.hir_locals^))
append(ctx.hir_locals, hir.Local{ append(ctx.hir_locals, hir.Local{
name = statement.name, type = value_type, mutable = !statement.immutable, name = statement.name, type = value_type, mutable = !statement.immutable,
@@ -4954,6 +5141,18 @@ build_block :: proc(
ctx.problematic^ = true ctx.problematic^ = true
continue 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^)) local_id := hir.local_id(len(ctx.hir_locals^))
append(ctx.hir_locals, hir.Local{ append(ctx.hir_locals, hir.Local{
name = statement.name, type = value_type, mutable = !statement.immutable, 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) local, found := find_build_local(ctx.locals^[:], statement.name)
if !found { 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(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{ append(&checker.module.statements, hir.Stmt{
kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR, kind=.Assignment, span=statement.span, local=hir.INVALID_LOCAL,
local = hir.INVALID_LOCAL, diagnostic = id, 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 continue
} }
if !local.mutable { if !local.mutable {
@@ -5343,6 +5575,12 @@ build_block :: proc(
"'if' unwrap captures must have distinct names", "'if' unwrap captures must have distinct names",
) )
valid_unwrap = false 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^)) local = hir.local_id(len(ctx.hir_locals^))
append(ctx.hir_locals, hir.Local{name=capture, type=child, mutable=false}) 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) condition = invalid_hir_expr(checker, statement.span, id, types.BOOL)
ctx.problematic^ = true 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_defer_starts, len(ctx.defers^))
append(ctx.loop_labels, statement.label) append(ctx.loop_labels, statement.label)
append(ctx.loop_is_loop, true) append(ctx.loop_is_loop, true)
@@ -5530,6 +5777,15 @@ build_block :: proc(
} }
capture_start := len(ctx.locals^) 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^)) 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.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}) 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 { if statement.index_name == statement.name {
diagnostic = source.add(checker.diagnostics, statement.span, "for-loop captures must have distinct names") diagnostic = source.add(checker.diagnostics, statement.span, "for-loop captures must have distinct names")
valid_loop = false 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 { } else {
index_local = hir.local_id(len(ctx.hir_locals^)) 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.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}) 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_defer_starts, len(ctx.defers^))
append(ctx.loop_labels, statement.label) append(ctx.loop_labels, statement.label)
append(ctx.loop_is_loop, true) append(ctx.loop_is_loop, true)
@@ -5636,6 +5907,15 @@ build_block :: proc(
// A labeled block statement (`blk: { … break :blk … }`): a break target // A labeled block statement (`blk: { … break :blk … }`): a break target
// with an exit-label boundary, built as a HIR `.Block`. Not a loop, so // with an exit-label boundary, built as a HIR `.Block`. Not a loop, so
// unlabeled `break`/`continue` and `continue :blk` skip it. // 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_defer_starts, len(ctx.defers^))
append(ctx.loop_labels, statement.label) append(ctx.loop_labels, statement.label)
append(ctx.loop_is_loop, false) 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 { 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") source.add(checker.diagnostics, if_stmt.span, "'if' unwrap captures must have distinct names")
ok = false 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^)) local = hir.local_id(len(ctx.hir_locals^))
append(ctx.hir_locals, hir.Local{name=capture, type=child, mutable=false}) 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: [dynamic]hir.Stmt_Id
result.allocator = checker.allocator result.allocator = checker.allocator
capture_start := len(ctx.locals^) capture_start := len(ctx.locals^)
capture_ok := true
if len(arm.captures) > 0 && field_index >= 0 { if len(arm.captures) > 0 && field_index >= 0 {
capture := arm.captures[0] capture := arm.captures[0]
if capture != checker.sink_symbol { 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 // 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 // loads it; a `|@cap|` capture binds a pointer to it (mutability follows the
// subject), aliasing the original storage via the subject location. // 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 { if !as_value {
built := build_block(ctx, arm.body) built := build_block(ctx, arm.body)
for s in built { for s in built {
@@ -6634,7 +6931,7 @@ build_match_arm_body :: proc(
} }
delete(built, checker.allocator) delete(built, checker.allocator)
} else { } 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) resize(ctx.locals, capture_start)
return result[:], body_ok return result[:], body_ok
@@ -6911,6 +7208,10 @@ build_value_labeled_block :: proc(
slot = new_value_slot(ctx, slot_type) 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{ append(ctx.yield_targets, Yield_Target{
label = label, slot = slot, slot_type = slot_type, result_optional = result_optional, label = label, slot = slot, slot_type = slot_type, result_optional = result_optional,
defer_floor = len(ctx.defers^), defer_floor = len(ctx.defers^),
@@ -7438,7 +7739,18 @@ build_globals :: proc(checker: ^Checker) {
global_type = checker.module.exprs[expr].type global_type = checker.module.exprs[expr].type
} }
diagnostic := source.INVALID_DIAGNOSTIC 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( diagnostic = source.addf(
checker.diagnostics, checker.diagnostics,
global.span, global.span,
@@ -7457,15 +7769,19 @@ build_globals :: proc(checker: ^Checker) {
expr = invalid_hir_expr(checker, global.span, diagnostic) expr = invalid_hir_expr(checker, global.span, diagnostic)
} }
if !global.immutable { if !global.immutable {
diagnostic = source.add( if !types.is_valid(global.type) {
checker.diagnostics, diagnostic = source.addf(
global.span, checker.diagnostics,
"mutable declarations are only valid inside functions", global.span,
) "mutable global '%s' requires a type annotation",
expr = invalid_hir_expr(checker, global.span, diagnostic) 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) 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)) _ = hir.global_id(len(checker.module.globals))
append( append(
&checker.module.globals, &checker.module.globals,
@@ -7477,7 +7793,7 @@ build_globals :: proc(checker: ^Checker) {
static_value = static_value, static_value = static_value,
is_static = is_static, is_static = is_static,
external = false, external = false,
writable = false, writable = !global.immutable,
dependencies = dependencies, dependencies = dependencies,
calls = calls[:], calls = calls[:],
direct_problem = expr_problematic(checker, expr), 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)) 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 global, index in ast_module.globals {
for previous in ast_module.globals[:index] { 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)) 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) validate_type_nodes(&checker)
+5
View File
@@ -1234,6 +1234,11 @@ declaration_conflicts :: proc(module: ^ast.Module, pkg: ast.Package_Id, name: sy
return true return true
} }
} }
type_id := types.find_named(&module.type_store, u32(pkg), u32(name))
type_item, type_ok := types.node(&module.type_store, type_id)
if type_ok && type_item.declared {
return true
}
return false return false
} }
+156 -9
View File
@@ -5480,6 +5480,157 @@ valid_runtime_global_initializes_before_main :: proc(t: ^testing.T) {
testing.expect_value(t, state.exit_code, 0) testing.expect_value(t, state.exit_code, 0)
} }
@(test)
mutable_runtime_globals_compile_and_run :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-mutable-global"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/mutable_global", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 52)
}
@(test)
mutable_globals_infer_constraints_and_emit_writable_storage :: proc(t: ^testing.T) {
text := `Point :: struct {
x i32
}
counter int = 0
ratio float = 1
span range = 0..2
point Point = Point { x = 1 }
values [_]mut i32 = [10, 20]
main func() void {
counter = 1
counter += 1
point.x = counter
values[1] = point.x
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
found_counter := false
found_ratio := false
found_span := false
found_values := false
for global in hir_module.globals {
name := symbol.resolve(&symbols, global.name)
if name == "counter" {
found_counter = global.writable && !global.is_static && types.equal(global.type, types.I8)
} else if name == "ratio" {
found_ratio = global.writable && !global.is_static && types.equal(global.type, types.F64)
} else if name == "span" {
found_span = global.writable && !global.is_static && types.is_range(global.type, &hir_module.types)
} else if name == "values" {
item, ok := types.node(&hir_module.types, global.type)
found_values = global.writable && !global.is_static && ok && item.kind == .Array && item.count == 2
}
}
testing.expect(t, found_counter)
testing.expect(t, found_ratio)
testing.expect(t, found_span)
testing.expect(t, found_values)
testing.expect(t, strings.contains(llvm_text, "internal global"))
testing.expect(t, !strings.contains(llvm_text, "internal constant i8 0"))
}
@(test)
mutable_global_diagnostics_and_no_shadowing :: proc(t: ^testing.T) {
text := `ID :: distinct i32
OtherID :: distinct i32
ID i32 = 0
counter = 0
bad i32 = undefined
runtime i32 = 1
immutable :: 1
foo func(foo i32) void {}
main func() void {
_ = $runtime
immutable = 2
OtherID i32 = 0
local i32 = 0
if true {
local i32 = 1
}
for 0..1 |local| {}
scope i32 = 0
scope: {}
mark: {
mark i32 = 0
}
value i32 = value_label: {
value_label i32 = 1
yield :value_label 1
}
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
missing_type := false
undefined_global := false
not_comptime := false
immutable_write := false
global_type_shadow := false
param_shadow := false
local_type_shadow := false
local_shadow := false
capture_shadow := false
label_shadow := false
local_label_shadow := false
value_label_shadow := false
for diagnostic in diagnostics.items {
missing_type = missing_type || strings.contains(diagnostic.message, "mutable global 'counter' requires a type annotation")
undefined_global = undefined_global || strings.contains(diagnostic.message, "'undefined' is only valid as a mutable local declaration initializer")
not_comptime = not_comptime || strings.contains(diagnostic.message, "global 'runtime' is not comptime-known")
immutable_write = immutable_write || strings.contains(diagnostic.message, "assignment target is not writable")
global_type_shadow = global_type_shadow || strings.contains(diagnostic.message, "global 'ID' shadows visible type")
param_shadow = param_shadow || strings.contains(diagnostic.message, "parameter 'foo' shadows visible function")
local_type_shadow = local_type_shadow || strings.contains(diagnostic.message, "local 'OtherID' shadows visible type")
local_shadow = local_shadow || strings.contains(diagnostic.message, "local 'local' shadows visible local")
capture_shadow = capture_shadow || strings.contains(diagnostic.message, "capture 'local' shadows visible local")
label_shadow = label_shadow || strings.contains(diagnostic.message, "label 'scope' shadows visible local")
local_label_shadow = local_label_shadow || strings.contains(diagnostic.message, "local 'mark' shadows visible label")
value_label_shadow = value_label_shadow || strings.contains(diagnostic.message, "local 'value_label' shadows visible label")
}
testing.expect(t, missing_type)
testing.expect(t, undefined_global)
testing.expect(t, not_comptime)
testing.expect(t, immutable_write)
testing.expect(t, global_type_shadow)
testing.expect(t, param_shadow)
testing.expect(t, local_type_shadow)
testing.expect(t, local_shadow)
testing.expect(t, capture_shadow)
testing.expect(t, label_shadow)
testing.expect(t, local_label_shadow)
testing.expect(t, value_label_shadow)
}
@(test) @(test)
unused_global_cycle_is_deferred :: proc(t: ^testing.T) { unused_global_cycle_is_deferred :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-cycle-unused" output := "/tmp/brolang-test-cycle-unused"
@@ -8153,7 +8304,7 @@ main func() void {
} }
@(test) @(test)
distinct_type_construction_defers_to_callable_names :: proc(t: ^testing.T) { type_and_function_names_cannot_shadow :: proc(t: ^testing.T) {
text := `Value :: distinct u32 text := `Value :: distinct u32
Value func(value i32) i32 { Value func(value i32) i32 {
return value return value
@@ -8174,15 +8325,11 @@ main func() i32 {
hir_module := checker.check(&ast_module, &diagnostics, &symbols) hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module) defer hir.destroy_module(&hir_module)
found_call := false found := false
found_retype := false for diagnostic in diagnostics.items {
for expr in hir_module.exprs { found = found || strings.contains(diagnostic.message, "function 'Value' shadows visible type")
found_call = found_call || expr.kind == .Call
found_retype = found_retype || expr.kind == .Retype
} }
testing.expect_value(t, len(diagnostics.items), 0) testing.expect(t, found)
testing.expect(t, found_call)
testing.expect(t, !found_retype)
} }
@(test) @(test)
+3 -3
View File
@@ -44,11 +44,11 @@ main func() i32 {
total = total + 5 # 35 total = total + 5 # 35
} }
# block scoping: inner x shadows outer x, outer is unchanged after the block # block scoping: inner bindings do not escape the block
x i32 = 1 x i32 = 1
if x == 1 { if x == 1 {
x i32 = 100 inner_x i32 = 100
if x == 100 { if inner_x == 100 {
total = total + 5 # 40 total = total + 5 # 40
} }
} }
@@ -1,4 +1,4 @@
bad int = 1 bad i32 :: undefined
read_bad func() int { read_bad func() int {
return bad return bad
@@ -1,4 +1,4 @@
bad int = 1 bad i32 :: undefined
read_bad func() int { read_bad func() int {
return bad return bad
+30
View File
@@ -0,0 +1,30 @@
Point :: struct {
x i32
}
counter int = 0
ratio float = 1
span range = 0..2
point Point = Point { x = 1 }
values [_]mut i32 = [10, 20]
bump func(value @mut i32) void {
value^ += 1
}
main func() i32 {
counter = 10
counter += 5
bump(&counter)
point.x += counter
values[1] = point.x
total i32 = counter + point.x + values[1]
for span |i| {
total += i
}
if ratio == 1.0 {
total += 1
}
return total
}
+3 -3
View File
@@ -31,12 +31,12 @@ main func() i32 {
} }
} }
# The body-local k shadows only inside the body. The update still targets # Body-local storage stays scoped to the body. The update still targets
# the mutable k declared before the loop. # the mutable k declared before the loop.
k u32 = 0 k u32 = 0
while k < 4 : k = k + 1 { while k < 4 : k = k + 1 {
k u32 = 100 body_k u32 = 100
if k == 100 { if body_k == 100 {
total = total + 2 total = total + 2
} }
} }