diff --git a/LANGUAGE.md b/LANGUAGE.md index 46bec05..2aebf98 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -8,8 +8,8 @@ roadmap and milestone history. ### source, declarations, and packages - newline-terminated statements and `#` comments -- immutable `::` bindings, mutable function-local `=` bindings, and `_` sinks -- immutable package globals, function-local mutable locals, and mutable local declarations initialized with `undefined` +- immutable `::` bindings, typed mutable `=` locals/globals, and `_` sinks +- 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` - directory packages with merged declarations - 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 - 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 - replaceable dynamically loaded libclang C-import backend - C-header import caching by canonical path, target, include paths, and defines diff --git a/README.md b/README.md index 42fd7a6..4130b7e 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ Current prototype features: - Newline-terminated, multiline statements; `}` may terminate a block's final statement - `#` 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` - 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 @@ -143,7 +143,7 @@ Current prototype features: - Bodyless manual and imported C variadic declarations with default argument promotions - Ordered linking of additional C sources, objects, archives, and libraries - 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` See [LANGUAGE.md](LANGUAGE.md) for the concise implemented and planned language diff --git a/TODO.md b/TODO.md index 89b44a8..2092cb3 100644 --- a/TODO.md +++ b/TODO.md @@ -91,7 +91,7 @@ - operators: `and`, `or`, `!` - lazy evaluation / short-circuit evaluation - 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) - 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` @@ -403,8 +403,7 @@ statements first (a throwaway probe), so a first concrete `yield :blk` that references a block local still resolves the result to `?T` - 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); - same-label loop/block shadowing resolves innermost-wins + loop) whose concrete yield references a local declared *past* the first yield (annotate) 21. unions and tagged unions (implemented; first pass — native untagged unions only; see below) - inspired by zig @@ -683,6 +682,32 @@ stable aggregate serialization, comptime pointers/slices, and calls through 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) ## A word on multi-unwrap diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index baac6c5..c33476b 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -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) diff --git a/compiler/loader/loader.odin b/compiler/loader/loader.odin index 6b14d8b..995d054 100644 --- a/compiler/loader/loader.odin +++ b/compiler/loader/loader.odin @@ -1234,6 +1234,11 @@ declaration_conflicts :: proc(module: ^ast.Module, pkg: ast.Package_Id, name: sy 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 } diff --git a/compiler_tests.odin b/compiler_tests.odin index ac98d6a..1999753 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -5480,6 +5480,157 @@ valid_runtime_global_initializes_before_main :: proc(t: ^testing.T) { 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) unused_global_cycle_is_deferred :: proc(t: ^testing.T) { output := "/tmp/brolang-test-cycle-unused" @@ -8153,7 +8304,7 @@ main func() void { } @(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 Value func(value i32) i32 { return value @@ -8174,15 +8325,11 @@ main func() i32 { hir_module := checker.check(&ast_module, &diagnostics, &symbols) defer hir.destroy_module(&hir_module) - found_call := false - found_retype := false - for expr in hir_module.exprs { - found_call = found_call || expr.kind == .Call - found_retype = found_retype || expr.kind == .Retype + found := false + for diagnostic in diagnostics.items { + found = found || strings.contains(diagnostic.message, "function 'Value' shadows visible type") } - testing.expect_value(t, len(diagnostics.items), 0) - testing.expect(t, found_call) - testing.expect(t, !found_retype) + testing.expect(t, found) } @(test) diff --git a/examples/programs/control_flow/main.bro b/examples/programs/control_flow/main.bro index 94464ad..77bbba4 100644 --- a/examples/programs/control_flow/main.bro +++ b/examples/programs/control_flow/main.bro @@ -44,11 +44,11 @@ main func() i32 { 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 if x == 1 { - x i32 = 100 - if x == 100 { + inner_x i32 = 100 + if inner_x == 100 { total = total + 5 # 40 } } diff --git a/examples/programs/function_global_unused/main.bro b/examples/programs/function_global_unused/main.bro index e072a43..d670fe2 100644 --- a/examples/programs/function_global_unused/main.bro +++ b/examples/programs/function_global_unused/main.bro @@ -1,4 +1,4 @@ -bad int = 1 +bad i32 :: undefined read_bad func() int { return bad diff --git a/examples/programs/function_global_used/main.bro b/examples/programs/function_global_used/main.bro index f5eea79..2644b5d 100644 --- a/examples/programs/function_global_used/main.bro +++ b/examples/programs/function_global_used/main.bro @@ -1,4 +1,4 @@ -bad int = 1 +bad i32 :: undefined read_bad func() int { return bad diff --git a/examples/programs/mutable_global/main.bro b/examples/programs/mutable_global/main.bro new file mode 100644 index 0000000..ee2960e --- /dev/null +++ b/examples/programs/mutable_global/main.bro @@ -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 +} diff --git a/examples/programs/while_loop/main.bro b/examples/programs/while_loop/main.bro index 22e16f4..277a52a 100644 --- a/examples/programs/while_loop/main.bro +++ b/examples/programs/while_loop/main.bro @@ -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. k u32 = 0 while k < 4 : k = k + 1 { - k u32 = 100 - if k == 100 { + body_k u32 = 100 + if body_k == 100 { total = total + 2 } }