From 220b1c6e82159b40ba57c79b420e631302701027 Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Sat, 11 Jul 2026 23:37:30 +0200 Subject: [PATCH] `_` prefixed top-level symbols now file-local --- LANGUAGE.md | 1 + compiler/ast/ast.odin | 2 + compiler/checker/checker.odin | 158 ++++++++++++------- compiler/checker/comptime.odin | 12 +- compiler/loader/loader.odin | 10 +- compiler/parser/parser.odin | 19 ++- compiler/types/types.odin | 25 ++- compiler_tests.odin | 43 +++++ examples/packages/hidden_invalid/app/a.bro | 13 ++ examples/packages/hidden_invalid/app/b.bro | 17 ++ examples/packages/hidden_invalid/dep/dep.bro | 3 + examples/packages/hidden_valid/app/a.bro | 22 +++ examples/packages/hidden_valid/app/b.bro | 20 +++ examples/packages/hidden_valid/app/main.bro | 3 + std/mem/mem.bro | 24 +-- 15 files changed, 282 insertions(+), 90 deletions(-) create mode 100644 examples/packages/hidden_invalid/app/a.bro create mode 100644 examples/packages/hidden_invalid/app/b.bro create mode 100644 examples/packages/hidden_invalid/dep/dep.bro create mode 100644 examples/packages/hidden_valid/app/a.bro create mode 100644 examples/packages/hidden_valid/app/b.bro create mode 100644 examples/packages/hidden_valid/app/main.bro diff --git a/LANGUAGE.md b/LANGUAGE.md index 2905f34..4641034 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -13,6 +13,7 @@ roadmap and milestone history. - 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 +- native top-level declarations beginning with `_` are visible only within their source file; locals, fields, parameters, and C declarations are unaffected - relative `.h` imports as synthetic C header package namespaces - root `main` validation with trap executable recovery for missing or unusable entry points diff --git a/compiler/ast/ast.odin b/compiler/ast/ast.odin index 204451e..2dbb1c8 100644 --- a/compiler/ast/ast.odin +++ b/compiler/ast/ast.odin @@ -211,6 +211,7 @@ Function :: struct { c_abi: bool, imported: bool, generated: bool, + file_hidden: bool, has_body: bool, variadic: bool, params: []Param, @@ -232,6 +233,7 @@ Global :: struct { immutable: bool, external: bool, writable: bool, + file_hidden: bool, expr: Expr_Id, diagnostic: source.Diagnostic_Id, } diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index b23db84..4d49533 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -110,12 +110,16 @@ Build_Ctx :: struct { Function_Index_Entry :: struct { scope: ast.Package_Id, + file: ast.File_Id, + hidden: bool, name: symbol.Id, id: ast.Function_Id, } Global_Index_Entry :: struct { scope: ast.Package_Id, + file: ast.File_Id, + hidden: bool, name: symbol.Id, id: ast.Global_Id, } @@ -617,7 +621,7 @@ import_index_less :: proc(left, right: Import_Index_Entry) -> bool { return left.id < right.id } -find_function_symbol :: proc(index: []Function_Index_Entry, scope: ast.Package_Id, name: symbol.Id) -> ast.Function_Id { +find_function_symbol :: proc(index: []Function_Index_Entry, scope: ast.Package_Id, name: symbol.Id, file := ast.INVALID_FILE) -> ast.Function_Id { low := 0 high := len(index) for low < high { @@ -629,13 +633,22 @@ find_function_symbol :: proc(index: []Function_Index_Entry, scope: ast.Package_I high = middle } } - if low < len(index) && index[low].scope == scope && index[low].name == name { - return index[low].id + visible := ast.INVALID_FUNCTION + for low < len(index) && index[low].scope == scope && index[low].name == name { + entry := index[low] + if entry.hidden { + if entry.file == file { + return entry.id + } + } else { + visible = entry.id + } + low += 1 } - return ast.INVALID_FUNCTION + return visible } -find_global_symbol :: proc(index: []Global_Index_Entry, scope: ast.Package_Id, name: symbol.Id) -> ast.Global_Id { +find_global_symbol :: proc(index: []Global_Index_Entry, scope: ast.Package_Id, name: symbol.Id, file := ast.INVALID_FILE) -> ast.Global_Id { low := 0 high := len(index) for low < high { @@ -647,10 +660,19 @@ find_global_symbol :: proc(index: []Global_Index_Entry, scope: ast.Package_Id, n high = middle } } - if low < len(index) && index[low].scope == scope && index[low].name == name { - return index[low].id + visible := ast.INVALID_GLOBAL + for low < len(index) && index[low].scope == scope && index[low].name == name { + entry := index[low] + if entry.hidden { + if entry.file == file { + return entry.id + } + } else { + visible = entry.id + } + low += 1 } - return ast.INVALID_GLOBAL + return visible } find_import_symbol :: proc(index: []Import_Index_Entry, scope: ast.File_Id, name: symbol.Id) -> ast.Import_Id { @@ -684,14 +706,14 @@ build_symbol_indexes :: proc(checker: ^Checker) { if function.generated { continue } - checker.function_index[function_index] = Function_Index_Entry{scope=function.pkg, name=function.name, id=ast.function_id(id)} + checker.function_index[function_index] = Function_Index_Entry{scope=function.pkg, file=function.file, hidden=function.file_hidden, name=function.name, id=ast.function_id(id)} function_index += 1 } slice.sort_by(checker.function_index, function_index_less) checker.global_index = make([]Global_Index_Entry, len(checker.ast_module.globals), checker.allocator) for global, id in checker.ast_module.globals { - checker.global_index[id] = Global_Index_Entry{scope=global.pkg, name=global.name, id=ast.global_id(id)} + checker.global_index[id] = Global_Index_Entry{scope=global.pkg, file=global.file, hidden=global.file_hidden, name=global.name, id=ast.global_id(id)} } slice.sort_by(checker.global_index, global_index_less) @@ -702,12 +724,12 @@ build_symbol_indexes :: proc(checker: ^Checker) { slice.sort_by(checker.import_index, import_index_less) } -find_template :: proc(checker: ^Checker, name: symbol.Id, pkg := ast.Package_Id(0)) -> ast.Function_Id { - return find_function_symbol(checker.function_index, pkg, name) +find_template :: proc(checker: ^Checker, name: symbol.Id, pkg := ast.Package_Id(0), file := ast.INVALID_FILE) -> ast.Function_Id { + return find_function_symbol(checker.function_index, pkg, name, file) } -find_global :: proc(checker: ^Checker, name: symbol.Id, pkg := ast.Package_Id(0)) -> ast.Global_Id { - return find_global_symbol(checker.global_index, pkg, name) +find_global :: proc(checker: ^Checker, name: symbol.Id, pkg := ast.Package_Id(0), file := ast.INVALID_FILE) -> ast.Global_Id { + return find_global_symbol(checker.global_index, pkg, name, file) } find_import :: proc(checker: ^Checker, file: ast.File_Id, alias: symbol.Id, mark_used := false) -> ast.Import_Id { @@ -718,12 +740,26 @@ 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)) +declared_type_named :: proc(checker: ^Checker, pkg: ast.Package_Id, name: symbol.Id, file := ast.INVALID_FILE) -> bool { + id := types.find_named(&checker.module.types, u32(pkg), u32(name), file=u32(file)) item, ok := types.node(&checker.module.types, id) return ok && item.declared } +declarations_conflict :: proc(left_file: ast.File_Id, left_hidden: bool, right_file: ast.File_Id, right_hidden: bool) -> bool { + return left_file == right_file if left_hidden && right_hidden else true +} + +type_declaration_conflicts :: proc(checker: ^Checker, pkg: ast.Package_Id, name: symbol.Id, file: ast.File_Id, hidden: bool) -> bool { + for item in checker.module.types.nodes { + if item.declared && item.pkg == u32(pkg) && item.name == u32(name) && + declarations_conflict(file, hidden, ast.File_Id(item.file), item.file_hidden) { + return true + } + } + return false +} + visible_name_kind :: proc( checker: ^Checker, name: symbol.Id, @@ -755,13 +791,13 @@ visible_name_kind :: proc( if find_import(checker, file, name) != ast.INVALID_IMPORT { return "import" } - if find_global(checker, name, pkg) != ast.INVALID_GLOBAL { + if find_global(checker, name, pkg, file) != ast.INVALID_GLOBAL { return "global" } - if find_template(checker, name, pkg) != ast.INVALID_FUNCTION { + if find_template(checker, name, pkg, file) != ast.INVALID_FUNCTION { return "function" } - if declared_type_named(checker, pkg, name) { + if declared_type_named(checker, pkg, name, file) { return "type" } return "" @@ -831,6 +867,10 @@ expr_package :: proc(checker: ^Checker, expr: ast.Expr, pkg: ast.Package_Id, fil return import_item.target, true } +expr_lookup_file :: proc(expr: ast.Expr, file: ast.File_Id) -> ast.File_Id { + return ast.INVALID_FILE if symbol.is_valid(expr.qualifier) else file +} + add_package_resolution_diagnostic :: proc(checker: ^Checker, expr: ast.Expr, file: ast.File_Id) -> source.Diagnostic_Id { if find_import(checker, file, expr.qualifier) == ast.INVALID_IMPORT { return source.addf(checker.diagnostics, expr.span, "unknown package alias '%s'", symbol_text(checker, expr.qualifier)) @@ -838,8 +878,8 @@ add_package_resolution_diagnostic :: proc(checker: ^Checker, expr: ast.Expr, fil return source.addf(checker.diagnostics, expr.span, "unavailable imported package '%s'", symbol_text(checker, expr.qualifier)) } -add_name_resolution_diagnostic :: proc(checker: ^Checker, expr: ast.Expr, target_pkg: ast.Package_Id) -> source.Diagnostic_Id { - if find_template(checker, expr.name, target_pkg) != ast.INVALID_FUNCTION { +add_name_resolution_diagnostic :: proc(checker: ^Checker, expr: ast.Expr, target_pkg: ast.Package_Id, file: ast.File_Id) -> source.Diagnostic_Id { + if find_template(checker, expr.name, target_pkg, expr_lookup_file(expr, file)) != ast.INVALID_FUNCTION { return source.addf(checker.diagnostics, expr.span, "'%s' is a function, not a global value", symbol_text(checker, expr.name)) } if symbol.is_valid(expr.qualifier) { @@ -854,8 +894,8 @@ add_name_resolution_diagnostic :: proc(checker: ^Checker, expr: ast.Expr, target return source.addf(checker.diagnostics, expr.span, "unresolved global '%s'", symbol_text(checker, expr.name)) } -add_call_resolution_diagnostic :: proc(checker: ^Checker, expr: ast.Expr, target_pkg: ast.Package_Id) -> source.Diagnostic_Id { - if find_global(checker, expr.name, target_pkg) != ast.INVALID_GLOBAL { +add_call_resolution_diagnostic :: proc(checker: ^Checker, expr: ast.Expr, target_pkg: ast.Package_Id, file: ast.File_Id) -> source.Diagnostic_Id { + if find_global(checker, expr.name, target_pkg, expr_lookup_file(expr, file)) != ast.INVALID_GLOBAL { return source.addf(checker.diagnostics, expr.span, "'%s' is a global, not a function", symbol_text(checker, expr.name)) } if symbol.is_valid(expr.qualifier) { @@ -976,7 +1016,7 @@ resolve_type_argument :: proc( if !available { return types.INVALID, false } - value := types.find_named(&checker.module.types, u32(target_pkg), u32(expr.name)) + value := types.find_named(&checker.module.types, u32(target_pkg), u32(expr.name), file=u32(expr_lookup_file(expr, file))) value = types.resolve_alias(value, &checker.module.types) return value, types.is_valid(value) } @@ -1963,7 +2003,7 @@ infer_compound_expr :: proc( return types.INVALID } target_pkg, available := expr_package(checker, expr, pkg, file) - value := types.find_named(store, u32(target_pkg), u32(expr.name)) if available else types.INVALID + value := types.find_named(store, u32(target_pkg), u32(expr.name), file=u32(expr_lookup_file(expr, file))) if available else types.INVALID return types.resolve_alias(value, store) case .Keyed: return infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types) @@ -2069,7 +2109,7 @@ 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 { + if global := find_global(checker, expr.qualifier, pkg, file); global != ast.INVALID_GLOBAL { base_type := checker.global_types[global] item, has_item := types.container(base_type, &checker.module.types) field_name := symbol_text(checker, expr.name) @@ -2112,7 +2152,7 @@ infer_expr :: proc( if !types.is_valid(last) { target_pkg, available := expr_package(checker, expr, pkg, file) if available { - global := find_global(checker, expr.name, target_pkg) + global := find_global(checker, expr.name, target_pkg, expr_lookup_file(expr, file)) if global != ast.INVALID_GLOBAL { last = checker.global_types[global] } @@ -2121,7 +2161,7 @@ infer_expr :: proc( if !types.is_valid(last) { target_pkg, available := expr_package(checker, expr, pkg, file) if available { - template := find_template(checker, expr.name, target_pkg) + template := find_template(checker, expr.name, target_pkg, expr_lookup_file(expr, file)) if template != ast.INVALID_FUNCTION && len(checker.ast_module.functions[template].unsupported_reason) == 0 && checker.template_diagnostics[template] == source.INVALID_DIAGNOSTIC { @@ -2205,7 +2245,7 @@ infer_expr :: proc( target_pkg, available := expr_package(checker, expr, pkg, file) template := ast.INVALID_FUNCTION if available { - template = find_template(checker, expr.name, target_pkg) + template = find_template(checker, expr.name, target_pkg, expr_lookup_file(expr, file)) } if template == ast.INVALID_FUNCTION { callee_type := types.INVALID @@ -2213,14 +2253,14 @@ infer_expr :: proc( callee_type = find_infer_local(locals, expr.name) } if !types.is_valid(callee_type) && available { - global := find_global(checker, expr.name, target_pkg) + global := find_global(checker, expr.name, target_pkg, expr_lookup_file(expr, file)) if global != ast.INVALID_GLOBAL { callee_type = checker.global_types[global] } } _, function_item, function_type, ok := types.function_pointer(callee_type, &checker.module.types) if !ok { - distinct_type := types.find_named(&checker.module.types, u32(target_pkg), u32(expr.name)) + distinct_type := types.find_named(&checker.module.types, u32(target_pkg), u32(expr.name), file=u32(expr_lookup_file(expr, file))) distinct_item, distinct_ok := types.node(&checker.module.types, distinct_type) if available && distinct_ok && distinct_item.kind == .Distinct && len(expr.args) == 1 { stack[frame_index].left = distinct_type @@ -2585,7 +2625,7 @@ 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 { + } else if global := find_global(checker, statement.name, pkg, file); global != ast.INVALID_GLOBAL { _ = merge_global_demand(checker, global, value_type) if !rhs_is_arith { _ = record_demand(checker, statement.expr, checker.global_types[global], locals^[:], local_types, pkg, file) @@ -2945,7 +2985,7 @@ open_const_default_type :: proc( if !available { return types.INVALID } - global := find_global(checker, expr.name, target_pkg) + global := find_global(checker, expr.name, target_pkg, expr_lookup_file(expr, file)) index := int(global) if global == ast.INVALID_GLOBAL || index < 0 || index >= len(checker.global_open_const) { return types.INVALID @@ -3022,7 +3062,7 @@ expr_accepts_numeric_demand :: proc( if !available { return false } - global := find_global(checker, expr.name, target_pkg) + global := find_global(checker, expr.name, target_pkg, expr_lookup_file(expr, file)) index := int(global) if global == ast.INVALID_GLOBAL || index < 0 || index >= len(checker.global_open_const) { return false @@ -3112,7 +3152,7 @@ record_demand :: proc( if !available { return false } - global := find_global(checker, expr.name, target_pkg) + global := find_global(checker, expr.name, target_pkg, expr_lookup_file(expr, file)) if global != ast.INVALID_GLOBAL { return merge_global_demand(checker, global, demand) } @@ -3791,7 +3831,7 @@ infer_qualified_value_field_type :: proc( } base_type := find_infer_local(locals, expr.qualifier) if !types.is_valid(base_type) { - if global := find_global(checker, expr.qualifier, pkg); global != ast.INVALID_GLOBAL { + if global := find_global(checker, expr.qualifier, pkg, file); global != ast.INVALID_GLOBAL { base_type = checker.global_types[global] } } @@ -3866,7 +3906,7 @@ build_qualified_value_field :: proc( value, ok := build_field_from_value(checker, expr, base, local.type) return value, true, ok } - if global := find_global(checker, expr.qualifier, pkg); global != ast.INVALID_GLOBAL { + if global := find_global(checker, expr.qualifier, pkg, file); global != ast.INVALID_GLOBAL { base := build_global_reference(checker, global, expr.span, global_reads) value, ok := build_field_from_value(checker, expr, base, checker.global_types[global]) return value, true, ok @@ -3893,7 +3933,7 @@ enum_type_from_name_expr :: proc( find_import(checker, file, expr.qualifier) != ast.INVALID_IMPORT { return types.INVALID, false } - enum_type := types.find_named(&checker.module.types, u32(pkg), u32(expr.qualifier)) + enum_type := types.find_named(&checker.module.types, u32(pkg), u32(expr.qualifier), file=u32(file)) return enum_type, types.is_enum(enum_type, &checker.module.types) } @@ -3915,7 +3955,7 @@ enum_type_from_field_expr :: proc( if !available { return types.INVALID, false } - enum_type := types.find_named(&checker.module.types, u32(target_pkg), u32(base.name)) + enum_type := types.find_named(&checker.module.types, u32(target_pkg), u32(base.name), file=u32(expr_lookup_file(base, file))) return enum_type, types.is_enum(enum_type, &checker.module.types) } @@ -4615,7 +4655,7 @@ build_compound_expr :: proc( struct_type := types.INVALID if symbol.is_valid(expr.name) { target_pkg, available := expr_package(checker, expr, pkg, file, true) - struct_type = types.find_named(store, u32(target_pkg), u32(expr.name)) if available else types.INVALID + struct_type = types.find_named(store, u32(target_pkg), u32(expr.name), file=u32(expr_lookup_file(expr, file))) if available else types.INVALID struct_type = types.resolve_alias(struct_type, store) if !types.is_record(struct_type, store) || types.is_opaque_struct(struct_type, store) { id := source.addf(checker.diagnostics, expr.span, "unknown or opaque record type '%s'", symbol_text(checker, expr.name)) @@ -4853,7 +4893,7 @@ 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 { + if global := find_global(checker, expr.qualifier, pkg, file); global != ast.INVALID_GLOBAL { base := build_global_reference(checker, global, expr.span, global_reads) base_type := checker.global_types[global] item, has_item := types.container(base_type, &checker.module.types) @@ -4924,16 +4964,16 @@ build_expr :: proc( if !available { id := add_package_resolution_diagnostic(checker, expr, file) last = invalid_hir_expr(checker, expr.span, id) - } else if global := find_global(checker, expr.name, target_pkg); global != ast.INVALID_GLOBAL { + } else if global := find_global(checker, expr.name, target_pkg, expr_lookup_file(expr, file)); global != ast.INVALID_GLOBAL { last = build_global_reference(checker, global, expr.span, global_reads) } else { - template := find_template(checker, expr.name, target_pkg) + template := find_template(checker, expr.name, target_pkg, expr_lookup_file(expr, file)) if template != ast.INVALID_FUNCTION { last = build_function_value(checker, template, expr.span, frame.expected) } else { id := add_unsupported_diagnostic(checker, expr.span, target_pkg, expr.name) if id == source.INVALID_DIAGNOSTIC { - id = add_name_resolution_diagnostic(checker, expr, target_pkg) + id = add_name_resolution_diagnostic(checker, expr, target_pkg, file) } last = invalid_hir_expr(checker, expr.span, id) } @@ -5005,7 +5045,7 @@ build_expr :: proc( _ = pop(&stack) continue } - template := find_template(checker, expr.name, target_pkg) + template := find_template(checker, expr.name, target_pkg, expr_lookup_file(expr, file)) if template == ast.INVALID_FUNCTION { callee := hir.INVALID_EXPR non_callable := false @@ -5020,7 +5060,7 @@ build_expr :: proc( } } if callee == hir.INVALID_EXPR && !non_callable { - if global := find_global(checker, expr.name, target_pkg); global != ast.INVALID_GLOBAL { + if global := find_global(checker, expr.name, target_pkg, expr_lookup_file(expr, file)); global != ast.INVALID_GLOBAL { if _, _, _, callable := types.function_pointer(checker.global_types[global], &checker.module.types); callable { callee = build_global_reference(checker, global, expr.span, global_reads) } else { @@ -5030,7 +5070,7 @@ build_expr :: proc( } } if callee == hir.INVALID_EXPR { - distinct_type := types.find_named(&checker.module.types, u32(target_pkg), u32(expr.name)) + distinct_type := types.find_named(&checker.module.types, u32(target_pkg), u32(expr.name), file=u32(expr_lookup_file(expr, file))) distinct_item, distinct_ok := types.node(&checker.module.types, distinct_type) if distinct_ok && distinct_item.kind == .Distinct { if !is_runtime_type(checker, distinct_type) { @@ -5067,12 +5107,12 @@ build_expr :: proc( } id := source.INVALID_DIAGNOSTIC if non_callable { - id = add_call_resolution_diagnostic(checker, expr, target_pkg) if non_callable_global else + id = add_call_resolution_diagnostic(checker, expr, target_pkg, file) if non_callable_global else source.add(checker.diagnostics, expr.span, "call target is not a function pointer") } else { id = add_unsupported_diagnostic(checker, expr.span, target_pkg, expr.name) if id == source.INVALID_DIAGNOSTIC { - id = add_call_resolution_diagnostic(checker, expr, target_pkg) + id = add_call_resolution_diagnostic(checker, expr, target_pkg, file) } } last = invalid_hir_expr(checker, expr.span, id) @@ -5475,6 +5515,9 @@ make_link_name :: proc(checker: ^Checker, id: Spec_Id) -> string { defer strings.builder_destroy(&builder) strings.write_string(&builder, "bro_c__" if function.c_abi else "bro__") fmt.sbprintf(&builder, "p%d__", function.pkg) + if function.file_hidden { + fmt.sbprintf(&builder, "f%d__", function.file) + } strings.write_string(&builder, symbol_text(checker, function.name)) for arg in spec.args { strings.write_string(&builder, "__") @@ -5823,7 +5866,7 @@ build_block :: proc( } local, found := find_build_local(ctx.locals^[:], statement.name) if !found { - global := find_global(checker, statement.name, ctx.pkg) + global := find_global(checker, statement.name, ctx.pkg, ctx.file) if global == ast.INVALID_GLOBAL { id := source.addf(checker.diagnostics, statement.span, "cannot assign unresolved local '%s'", symbol_text(checker, statement.name)) append(&body, hir.stmt_id(len(checker.module.statements))) @@ -5967,7 +6010,7 @@ build_block :: proc( } } else if expr_ast.kind == .Struct_Literal { target_pkg, available := expr_package(checker, expr_ast, ctx.pkg, ctx.file, true) - named := types.find_named(store, u32(target_pkg), u32(expr_ast.name)) if available else types.INVALID + named := types.find_named(store, u32(target_pkg), u32(expr_ast.name), file=u32(expr_lookup_file(expr_ast, ctx.file))) if available else types.INVALID named = types.resolve_alias(named, store) error_path = can_implicitly_convert_type(checker, named, error_type) } else { @@ -8688,26 +8731,29 @@ check :: proc( continue } for previous in ast_module.functions[:index] { - if !previous.generated && previous.pkg == function.pkg && previous.name == function.name { + if !previous.generated && previous.pkg == function.pkg && previous.name == function.name && + declarations_conflict(function.file, function.file_hidden, previous.file, previous.file_hidden) { source.addf(diagnostics, function.span, "duplicate function '%s'", symbol_text(&checker, function.name)) } } for global in ast_module.globals { - if global.pkg == function.pkg && global.name == function.name { + if global.pkg == function.pkg && global.name == function.name && + declarations_conflict(function.file, function.file_hidden, global.file, global.file_hidden) { 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) { + if type_declaration_conflicts(&checker, function.pkg, function.name, function.file, function.file_hidden) { source.addf(diagnostics, function.span, "function '%s' shadows visible type", symbol_text(&checker, function.name)) } } for global, index in ast_module.globals { for previous in ast_module.globals[:index] { - if previous.pkg == global.pkg && previous.name == global.name { + if previous.pkg == global.pkg && previous.name == global.name && + declarations_conflict(global.file, global.file_hidden, previous.file, previous.file_hidden) { source.addf(diagnostics, global.span, "duplicate global '%s'", symbol_text(&checker, global.name)) } } - if declared_type_named(&checker, global.pkg, global.name) { + if type_declaration_conflicts(&checker, global.pkg, global.name, global.file, global.file_hidden) { source.addf(diagnostics, global.span, "global '%s' shadows visible type", symbol_text(&checker, global.name)) } } diff --git a/compiler/checker/comptime.odin b/compiler/checker/comptime.odin index bffa791..74fc34b 100644 --- a/compiler/checker/comptime.odin +++ b/compiler/checker/comptime.odin @@ -933,9 +933,9 @@ ct_eval_expr :: proc( if !available { return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "unavailable imported package") } - global := find_global(checker, expr.name, target_pkg) + global := find_global(checker, expr.name, target_pkg, expr_lookup_file(expr, state.file)) if global == ast.INVALID_GLOBAL || int(global) >= len(checker.ast_module.globals) { - template := find_template(checker, expr.name, target_pkg) + template := find_template(checker, expr.name, target_pkg, expr_lookup_file(expr, state.file)) if template != ast.INVALID_FUNCTION { pointer_type, _, function_ok := function_pointer_type_for_template( checker, @@ -1218,7 +1218,7 @@ ct_eval_struct_expr :: proc(state: ^Ct_State, expr: ast.Expr, expected: types.Ty struct_type := types.INVALID if symbol.is_valid(expr.name) { target_pkg, available := expr_package(checker, expr, state.pkg, state.file, false) - struct_type = types.find_named(store, u32(target_pkg), u32(expr.name)) if available else types.INVALID + struct_type = types.find_named(store, u32(target_pkg), u32(expr.name), file=u32(expr_lookup_file(expr, state.file))) if available else types.INVALID struct_type = types.resolve_alias(struct_type, store) } else { struct_type = types.resolve_alias(expected, store) @@ -1622,7 +1622,7 @@ ct_eval_place :: proc( if !available { return INVALID_CT_PLACE, types.INVALID, false, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "unavailable imported package") } - global := find_global(checker, expr.name, target_pkg) + global := find_global(checker, expr.name, target_pkg, expr_lookup_file(expr, state.file)) if global == ast.INVALID_GLOBAL || int(global) >= len(checker.ast_module.globals) { return INVALID_CT_PLACE, types.INVALID, false, ct_flow(.Normal), ct_failf(state, .Not_Comptime, expr.span, "unresolved comptime value '%s'", symbol_text(checker, expr.name)) } @@ -1926,7 +1926,7 @@ ct_eval_call_expr :: proc(state: ^Ct_State, expr: ast.Expr, expected: types.Type if !available { return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "unavailable function package") } - template := find_template(checker, expr.name, target_pkg) + template := find_template(checker, expr.name, target_pkg, expr_lookup_file(expr, state.file)) if template == ast.INVALID_FUNCTION || int(template) >= len(checker.ast_module.functions) { if !symbol.is_valid(expr.qualifier) { if index, ok := ct_find_binding_index(state, expr.name); ok { @@ -1936,7 +1936,7 @@ ct_eval_call_expr :: proc(state: ^Ct_State, expr: ast.Expr, expected: types.Type } } } - global := find_global(checker, expr.name, target_pkg) + global := find_global(checker, expr.name, target_pkg, expr_lookup_file(expr, state.file)) if global != ast.INVALID_GLOBAL && int(global) < len(checker.ast_module.globals) { g := checker.ast_module.globals[global] if !g.external && g.immutable && !g.writable { diff --git a/compiler/loader/loader.odin b/compiler/loader/loader.odin index fa68884..6bf375c 100644 --- a/compiler/loader/loader.odin +++ b/compiler/loader/loader.odin @@ -1226,18 +1226,18 @@ load_package :: proc(state: ^State, path: string, import_span: source.Span, is_r return pkg_id } -declaration_conflicts :: proc(module: ^ast.Module, pkg: ast.Package_Id, name: symbol.Id) -> bool { +declaration_conflicts :: proc(module: ^ast.Module, pkg: ast.Package_Id, file: ast.File_Id, name: symbol.Id) -> bool { for function in module.functions { - if function.pkg == pkg && function.name == name { + if function.pkg == pkg && function.name == name && (!function.file_hidden || function.file == file) { return true } } for global in module.globals { - if global.pkg == pkg && global.name == name { + if global.pkg == pkg && global.name == name && (!global.file_hidden || global.file == file) { return true } } - type_id := types.find_named(&module.type_store, u32(pkg), u32(name)) + type_id := types.find_named(&module.type_store, u32(pkg), u32(name), file=u32(file)) type_item, type_ok := types.node(&module.type_store, type_id) if type_ok && type_item.declared { return true @@ -1260,7 +1260,7 @@ validate_imports :: proc(state: ^State) { ) state.module.imports[import_id].valid = false } - if declaration_conflicts(state.module, import_item.pkg, alias) { + if declaration_conflicts(state.module, import_item.pkg, import_item.file, alias) { state.module.imports[import_id].diagnostic = source.addf( state.diagnostics, import_item.span, diff --git a/compiler/parser/parser.odin b/compiler/parser/parser.odin index c470aa4..2bc5d44 100644 --- a/compiler/parser/parser.odin +++ b/compiler/parser/parser.odin @@ -37,6 +37,11 @@ token_text :: proc(parser: ^Parser, tok: token.Token) -> string { return parser.source_file.text[int(tok.span.start):int(tok.span.end)] } +file_hidden_name :: proc(parser: ^Parser, tok: token.Token) -> bool { + name := token_text(parser, tok) + return len(name) > 1 && name[0] == '_' +} + span_from :: proc(first, last: source.Span) -> source.Span { return source.Span{file=first.file, start=first.start, end=last.end} } @@ -379,6 +384,7 @@ parse_type_atom :: proc(parser: ^Parser) -> ast.Type_Syntax { u32(name.symbol), u32(qualifier), u32(parser.file), + !symbol.is_valid(qualifier) && file_hidden_name(parser, name), ) } source.add(parser.diagnostics, tok.span, "expected a type") @@ -2108,6 +2114,7 @@ parse_function :: proc(parser: ^Parser, name: token.Token, c_abi: bool) { pkg=parser.pkg, file=parser.file, c_abi=c_abi, + file_hidden=!c_abi && file_hidden_name(parser, name), has_body=false, variadic=variadic, params=params, @@ -2126,6 +2133,7 @@ parse_function :: proc(parser: ^Parser, name: token.Token, c_abi: bool) { pkg=parser.pkg, file=parser.file, c_abi=c_abi, + file_hidden=!c_abi && file_hidden_name(parser, name), has_body=true, variadic=variadic, params=params, @@ -2272,7 +2280,7 @@ parse_inline_union_type :: proc(parser: ^Parser) -> types.Type { parse_struct :: proc(parser: ^Parser, name: token.Token, c_layout: bool, is_union := false) { start := advance(parser) - id := types.named(&parser.module.type_store, u32(parser.pkg), u32(name.symbol)) + id := types.named(&parser.module.type_store, u32(parser.pkg), u32(name.symbol), file=u32(parser.file), file_hidden=!c_layout && file_hidden_name(parser, name)) // A tagged union spells its discriminant in parens: `union(Enum)` reuses an existing // enum; `union(enum)` synthesizes one from the variant names after the body is parsed. tag := types.INVALID @@ -2330,7 +2338,7 @@ parse_struct :: proc(parser: ^Parser, name: token.Token, c_layout: bool, is_unio parse_opaque :: proc(parser: ^Parser, name: token.Token) { start := advance(parser) - id := types.named(&parser.module.type_store, u32(parser.pkg), u32(name.symbol)) + id := types.named(&parser.module.type_store, u32(parser.pkg), u32(name.symbol), file=u32(parser.file), file_hidden=file_hidden_name(parser, name)) if !types.define_record(&parser.module.type_store, id, nil, false, true, false) { source.addf(parser.diagnostics, name.span, "duplicate type declaration '%s'", token_text(parser, name)) } @@ -2359,7 +2367,7 @@ synthesize_union_tag :: proc(parser: ^Parser, fields: []types.Field) -> types.Ty parse_distinct :: proc(parser: ^Parser, name: token.Token) { start := advance(parser) child := parse_type(parser) - id := types.named(&parser.module.type_store, u32(parser.pkg), u32(name.symbol)) + id := types.named(&parser.module.type_store, u32(parser.pkg), u32(name.symbol), file=u32(parser.file), file_hidden=file_hidden_name(parser, name)) if !types.define_distinct(&parser.module.type_store, id, child) { source.addf(parser.diagnostics, name.span, "duplicate type declaration '%s'", token_text(parser, name)) } @@ -2372,7 +2380,7 @@ parse_distinct :: proc(parser: ^Parser, name: token.Token) { parse_alias :: proc(parser: ^Parser, name: token.Token) { start := advance(parser) child := parse_type(parser) - id := types.named(&parser.module.type_store, u32(parser.pkg), u32(name.symbol)) + id := types.named(&parser.module.type_store, u32(parser.pkg), u32(name.symbol), file=u32(parser.file), file_hidden=file_hidden_name(parser, name)) if !types.define_alias(&parser.module.type_store, id, child) { source.addf(parser.diagnostics, name.span, "duplicate type declaration '%s'", token_text(parser, name)) } @@ -2519,7 +2527,7 @@ parse_enum :: proc(parser: ^Parser, name: token.Token) { _ = finish_statement(parser) return } - id := types.named(&parser.module.type_store, u32(parser.pkg), u32(name.symbol)) + id := types.named(&parser.module.type_store, u32(parser.pkg), u32(name.symbol), file=u32(parser.file), file_hidden=file_hidden_name(parser, name)) if !types.define_enum(&parser.module.type_store, id, backing, members[:], explicit_backing) { source.addf(parser.diagnostics, name.span, "duplicate type declaration '%s'", token_text(parser, name)) } @@ -2699,6 +2707,7 @@ parse_top_level :: proc(parser: ^Parser) { name=name.symbol, pkg=parser.pkg, file=parser.file, + file_hidden=file_hidden_name(parser, name), type=type_syntax, immutable=operator.kind == .Colon_Colon, expr=expr, diff --git a/compiler/types/types.odin b/compiler/types/types.odin index 234422f..773e189 100644 --- a/compiler/types/types.odin +++ b/compiler/types/types.odin @@ -102,6 +102,7 @@ Node :: struct { c_layout: bool, opaque: bool, declared: bool, + file_hidden: bool, explicit_backing: bool, } @@ -185,24 +186,36 @@ intern :: proc(store: ^Store, candidate: Node) -> Type { return id } -named :: proc(store: ^Store, pkg, name: u32, qualifier: u32 = 0, file: u32 = 0xffff_ffff) -> Type { - normalized_file := file if qualifier != 0 else u32(0) +named :: proc(store: ^Store, pkg, name: u32, qualifier: u32 = 0, file: u32 = 0xffff_ffff, file_hidden := false) -> Type { + normalized_file := file if qualifier != 0 || file_hidden else u32(0) for existing, index in store.nodes { + visibility_matches := existing.file_hidden == file_hidden && + (!file_hidden || existing.file == normalized_file) if (existing.kind == .Named || existing.kind == .Alias || existing.kind == .Distinct || existing.kind == .Enum || existing.kind == .Struct || existing.kind == .Union) && existing.pkg == pkg && existing.name == name && existing.qualifier == qualifier && - existing.file == normalized_file { + (visibility_matches || qualifier == 0 && existing.file_hidden != file_hidden) { return DYNAMIC_START+Type(index) } } - return intern(store, Node{kind=.Named, pkg=pkg, name=name, qualifier=qualifier, file=normalized_file}) + return intern(store, Node{kind=.Named, pkg=pkg, name=name, qualifier=qualifier, file=normalized_file, file_hidden=file_hidden}) } -find_named :: proc(store: ^Store, pkg, name: u32, qualifier: u32 = 0) -> Type { +find_named :: proc(store: ^Store, pkg, name: u32, qualifier: u32 = 0, file: u32 = 0xffff_ffff) -> Type { + if file != 0xffff_ffff { + for existing, index in store.nodes { + if (existing.kind == .Named || existing.kind == .Alias || existing.kind == .Distinct || + existing.kind == .Enum || existing.kind == .Struct || existing.kind == .Union) && + existing.pkg == pkg && existing.name == name && existing.qualifier == qualifier && + existing.file_hidden && existing.file == file { + return DYNAMIC_START+Type(index) + } + } + } for existing, index in store.nodes { if (existing.kind == .Named || existing.kind == .Alias || existing.kind == .Distinct || existing.kind == .Enum || existing.kind == .Struct || existing.kind == .Union) && - existing.pkg == pkg && existing.name == name && existing.qualifier == qualifier { + existing.pkg == pkg && existing.name == name && existing.qualifier == qualifier && !existing.file_hidden { return DYNAMIC_START+Type(index) } } diff --git a/compiler_tests.odin b/compiler_tests.odin index ac750e8..1abf768 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -6645,6 +6645,49 @@ imports_are_file_local :: proc(t: ^testing.T) { testing.expect(t, !state.success) } +@(test) +leading_underscore_declarations_are_file_hidden :: proc(t: ^testing.T) { + output := "/tmp/brolang-test-hidden-declarations" + defer _ = os.remove(output) + status := compiler_core.compile_package("examples/packages/hidden_valid/app", output) + testing.expect_value(t, status, 0) + state := run_executable(output) + testing.expect_value(t, state.exit_code, 7) +} + +@(test) +file_hidden_declarations_are_not_package_members :: proc(t: ^testing.T) { + sources := source.init_store() + defer source.destroy_store(&sources) + diagnostics := source.init_store_diagnostics(&sources) + defer source.destroy_diagnostics(&diagnostics) + symbols := symbol.init_table() + defer symbol.destroy_table(&symbols) + module, loaded := loader.load("examples/packages/hidden_invalid/app", &sources, &diagnostics, &symbols) + defer ast.destroy_module(&module) + hir_module := checker.check(&module, &diagnostics, &symbols) + defer hir.destroy_module(&hir_module) + + testing.expect(t, loaded) + found_sibling := false + found_sibling_value := false + found_sibling_type := false + found_import := false + found_collision := false + for diagnostic in diagnostics.items { + found_sibling = found_sibling || strings.contains(diagnostic.message, "unresolved function '_sibling'") + found_sibling_value = found_sibling_value || strings.contains(diagnostic.message, "unresolved global '_sibling_value'") + found_sibling_type = found_sibling_type || strings.contains(diagnostic.message, "unknown or opaque record type '_Sibling'") + found_import = found_import || strings.contains(diagnostic.message, "package 'dep' has no member '_secret'") + found_collision = found_collision || strings.contains(diagnostic.message, "duplicate function '_collision'") + } + testing.expect(t, found_sibling) + testing.expect(t, found_sibling_value) + testing.expect(t, found_sibling_type) + testing.expect(t, found_import) + testing.expect(t, found_collision) +} + @(test) package_import_cycles_are_valid :: proc(t: ^testing.T) { output := "/tmp/brolang-test-package-cycle" diff --git a/examples/packages/hidden_invalid/app/a.bro b/examples/packages/hidden_invalid/app/a.bro new file mode 100644 index 0000000..e76a2ee --- /dev/null +++ b/examples/packages/hidden_invalid/app/a.bro @@ -0,0 +1,13 @@ +_sibling func() i32 { + return 1 +} + +_Sibling :: struct { + value i32 +} + +_sibling_value :: 1 + +_collision c_func() i32 { + return 1 +} diff --git a/examples/packages/hidden_invalid/app/b.bro b/examples/packages/hidden_invalid/app/b.bro new file mode 100644 index 0000000..82fe995 --- /dev/null +++ b/examples/packages/hidden_invalid/app/b.bro @@ -0,0 +1,17 @@ +import "../dep" + +_collision func() i32 { + return 2 +} + +read_sibling func(value _Sibling) i32 { + return value.value + _sibling_value +} + +read_sibling_value func() i32 { + return _sibling_value +} + +main func() i32 { + return _sibling() + dep._secret() + read_sibling(_Sibling { value = 1 }) + read_sibling_value() +} diff --git a/examples/packages/hidden_invalid/dep/dep.bro b/examples/packages/hidden_invalid/dep/dep.bro new file mode 100644 index 0000000..cda4f81 --- /dev/null +++ b/examples/packages/hidden_invalid/dep/dep.bro @@ -0,0 +1,3 @@ +_secret func() i32 { + return 1 +} diff --git a/examples/packages/hidden_valid/app/a.bro b/examples/packages/hidden_valid/app/a.bro new file mode 100644 index 0000000..79801bd --- /dev/null +++ b/examples/packages/hidden_valid/app/a.bro @@ -0,0 +1,22 @@ +_Thing :: struct { + value i32 +} + +_value :: 1 + +_helper func() i32 { + thing _Thing = _Thing { value = _value } + return thing.value +} + +_foreign c_func() i32 { + return 1 +} + +_C_Record :: c_struct { + value c_int +} + +from_a func() i32 { + return _helper() +} diff --git a/examples/packages/hidden_valid/app/b.bro b/examples/packages/hidden_valid/app/b.bro new file mode 100644 index 0000000..34cb621 --- /dev/null +++ b/examples/packages/hidden_valid/app/b.bro @@ -0,0 +1,20 @@ +_Thing :: struct { + value i32 +} + +_value :: 2 + +_helper func() i32 { + thing _Thing = _Thing { value = _value } + return thing.value +} + +Box :: struct { + _value i32 +} + +from_b func(_input i32) i32 { + _local Box = Box { _value = _input } + record _C_Record = _C_Record { value = 0 } + return _helper() + _local._value + _foreign() + i32(record.value) +} diff --git a/examples/packages/hidden_valid/app/main.bro b/examples/packages/hidden_valid/app/main.bro new file mode 100644 index 0000000..3f4b1e6 --- /dev/null +++ b/examples/packages/hidden_valid/app/main.bro @@ -0,0 +1,3 @@ +main func() i32 { + return from_a() + from_b(3) +} diff --git a/std/mem/mem.bro b/std/mem/mem.bro index 167676e..55aa389 100644 --- a/std/mem/mem.bro +++ b/std/mem/mem.bro @@ -7,6 +7,18 @@ Allocator :: struct { free @func(context ?*mut anyopaque, memory ?*mut u8, size usize, alignment usize) void } +alloc func(allocator Allocator, size usize, alignment usize) ?*mut u8 { + return allocator.alloc(allocator.context, size, alignment) +} + +realloc func(allocator Allocator, memory ?*mut u8, old_size usize, new_size usize, alignment usize) ?*mut u8 { + return allocator.realloc(allocator.context, memory, old_size, new_size, alignment) +} + +free func(allocator Allocator, memory ?*mut u8, size usize, alignment usize) void { + allocator.free(allocator.context, memory, size, alignment) +} + _malloc_alignment usize :: 16 # ponytail: aarch64-macos libc malloc alignment assumption. _power_of_two func(value usize) bool { @@ -87,15 +99,3 @@ c_allocator Allocator :: Allocator { realloc = _c_realloc, free = _c_free, } - -alloc func(allocator Allocator, size usize, alignment usize) ?*mut u8 { - return allocator.alloc(allocator.context, size, alignment) -} - -realloc func(allocator Allocator, memory ?*mut u8, old_size usize, new_size usize, alignment usize) ?*mut u8 { - return allocator.realloc(allocator.context, memory, old_size, new_size, alignment) -} - -free func(allocator Allocator, memory ?*mut u8, size usize, alignment usize) void { - allocator.free(allocator.context, memory, size, alignment) -}