From 2b90b616e19068c23bb92ca7cf1ccfbd600a70be Mon Sep 17 00:00:00 2001 From: gingerBill Date: Thu, 1 Oct 2026 16:24:01 +0100 Subject: [PATCH 01/17] Add `-vet-when-shadowing`; Resolve global `when`s and `foreign` blocks on demand via name placeholders --- src/build_settings.cpp | 5 +- src/checker.cpp | 289 +++------------------ src/checker.hpp | 2 +- src/checker_global_graph.cpp | 10 +- src/checker_global_when.cpp | 474 +++++++++++++++++++++++++++++++++++ src/main.cpp | 8 + src/parser.cpp | 1 + src/parser.hpp | 1 - 8 files changed, 529 insertions(+), 261 deletions(-) create mode 100644 src/checker_global_when.cpp diff --git a/src/build_settings.cpp b/src/build_settings.cpp index 041622a30..7f4da5140 100644 --- a/src/build_settings.cpp +++ b/src/build_settings.cpp @@ -317,10 +317,11 @@ enum VetFlags : u64 { VetFlag_Tabs = 1u<<9, VetFlag_UnusedProcedures = 1u<<10, VetFlag_ExplicitAllocators = 1u<<11, + VetFlag_WhenShadowing = 1u<<12, VetFlag_Unused = VetFlag_UnusedVariables|VetFlag_UnusedImports, - VetFlag_All = VetFlag_Unused|VetFlag_Shadowing|VetFlag_UsingStmt|VetFlag_Deprecated|VetFlag_Cast, + VetFlag_All = VetFlag_Unused|VetFlag_Shadowing|VetFlag_UsingStmt|VetFlag_Deprecated|VetFlag_Cast|VetFlag_WhenShadowing, VetFlag_Using = VetFlag_UsingStmt|VetFlag_UsingParam, }; @@ -352,6 +353,8 @@ u64 get_vet_flag_from_name(String const &name) { return VetFlag_UnusedProcedures; } else if (name == "explicit-allocators") { return VetFlag_ExplicitAllocators; + } else if (name == "when-shadowing") { + return VetFlag_WhenShadowing; } return VetFlag_NONE; } diff --git a/src/checker.cpp b/src/checker.cpp index e87c2ad59..27ef4b147 100644 --- a/src/checker.cpp +++ b/src/checker.cpp @@ -400,6 +400,8 @@ gb_internal void check_close_scope(CheckerContext *c) { } +gb_internal Entity *force_scope_placeholders(Scope *s, InternedString name, u32 hash); + gb_internal Entity *scope_lookup_current(Scope *s, InternedString name, u32 hash) { // Entity **found = string_map_get(&s->elements, name); if (hash == 0) { @@ -409,6 +411,9 @@ gb_internal Entity *scope_lookup_current(Scope *s, InternedString name, u32 hash if (found) { return found; } + if (s->placeholders != nullptr) { + return force_scope_placeholders(s, name, hash); + } return nullptr; } @@ -428,6 +433,9 @@ gb_internal void scope_lookup_parent(Scope *scope, InternedString name, Scope ** if (!is_single_threaded) rw_mutex_shared_lock(&s->mutex); found = scope_map_get(&s->elements, name, hash); if (!is_single_threaded) rw_mutex_shared_unlock(&s->mutex); + if (found == nullptr && s->placeholders != nullptr) { + found = force_scope_placeholders(s, name, hash); + } if (found) { Entity *e = found; if (gone_thru_proc) { @@ -5127,9 +5135,7 @@ gb_internal void check_collect_value_decl(CheckerContext *c, Ast *decl) { } } -gb_internal bool collect_file_decls(CheckerContext *ctx, Slice const &decls); - -gb_internal bool check_add_foreign_block_decl(CheckerContext *ctx, Ast *decl) { +gb_internal void check_add_foreign_block_decl(CheckerContext *ctx, Ast *decl) { ast_node(fb, ForeignBlockDecl, decl); Ast *foreign_library = fb->foreign_library; @@ -5144,11 +5150,7 @@ gb_internal bool check_add_foreign_block_decl(CheckerContext *ctx, Ast *decl) { check_decl_attributes(&c, fb->attributes, foreign_block_decl_attribute, nullptr); ast_node(block, BlockStmt, fb->body); - if (c.collect_delayed_decls && (c.scope->flags&ScopeFlag_File) != 0) { - return collect_file_decls(&c, block->stmts); - } check_collect_entities(&c, block->stmts); - return false; } gb_internal bool correct_single_type_alias(CheckerContext *c, Entity *e) { @@ -5156,7 +5158,11 @@ gb_internal bool correct_single_type_alias(CheckerContext *c, Entity *e) { DeclInfo *d = e->decl_info; if (d != nullptr && d->init_expr != nullptr) { Ast *init = d->init_expr; + // NOTE: in the scope of its own file, as a package's files may bind a name differently + Scope *prev_scope = c->scope; + c->scope = d->scope; Entity *alias_of = check_entity_from_ident_or_selector(c, init, true); + c->scope = prev_scope; if (alias_of != nullptr && alias_of->kind == Entity_TypeName) { e->kind = Entity_TypeName; return true; @@ -5189,7 +5195,7 @@ gb_internal bool correct_type_alias_in_scope_forwards(CheckerContext *c, Scope * } -gb_internal void correct_type_aliases_in_scope(CheckerContext *c, Scope *s) { +gb_internal void correct_type_aliases_in_package(CheckerContext *c, AstPackage *pkg) { // NOTE(bill, 2022-02-04): This is used to solve the problem caused by type aliases // of type aliases being "confused" as constants // @@ -5200,8 +5206,12 @@ gb_internal void correct_type_aliases_in_scope(CheckerContext *c, Scope *s) { // See @TypeAliasingProblem for more information for (;;) { bool corrections = false; - corrections |= correct_type_alias_in_scope_backwards(c, s); - corrections |= correct_type_alias_in_scope_forwards(c, s); + corrections |= correct_type_alias_in_scope_backwards(c, pkg->scope); + corrections |= correct_type_alias_in_scope_forwards(c, pkg->scope); + for (AstFile *f : pkg->files) { + corrections |= correct_type_alias_in_scope_backwards(c, f->scope); + corrections |= correct_type_alias_in_scope_forwards(c, f->scope); + } if (!corrections) { return; } @@ -5220,17 +5230,7 @@ gb_internal void check_collect_entities(CheckerContext *c, Slice const &n for_array(decl_index, nodes) { Ast *decl = nodes[decl_index]; if (!is_ast_decl(decl) && !is_ast_when_stmt(decl)) { - if (curr_file && decl->kind == Ast_ExprStmt) { - Ast *expr = decl->ExprStmt.expr; - if (expr->kind == Ast_CallExpr && expr->CallExpr.proc->kind == Ast_BasicDirective) { - if (c->collect_delayed_decls) { - if (decl->state_flags & StateFlag_BeenHandled) return; - decl->state_flags |= StateFlag_BeenHandled; - array_add(&curr_file->delayed_decls_queues[AstDelayQueue_Expr], expr); - } - continue; - } - } + // NOTE: global directives such as '#assert' are queued by `scan_global_decl_sources` continue; } @@ -5266,9 +5266,7 @@ gb_internal void check_collect_entities(CheckerContext *c, Slice const &n case_end; case_ast_node(fb, ForeignBlockDecl, decl); - if (curr_file != nullptr) { - array_add(&curr_file->delayed_decls_queues[AstDelayQueue_ForeignBlock], decl); - } + // NOTE: global ones are resolved like global 'when's, see `resolve_global_decl_sources` case_end; default: @@ -5785,7 +5783,6 @@ gb_internal void check_foreign_import_fullpaths(Checker *c) { AstFile *f = decl->file(); reset_checker_context(&ctx, f, &untyped); - ctx.collect_delayed_decls = false; GB_ASSERT(ctx.scope == e->scope); @@ -5935,165 +5932,6 @@ gb_internal void check_add_foreign_import_decl(CheckerContext *ctx, Ast *decl) { } -// Returns true if a new package is present -gb_internal bool collect_file_decls(CheckerContext *ctx, Slice const &decls); -gb_internal bool collect_file_decls_from_when_stmt(CheckerContext *ctx, AstWhenStmt *ws); - -gb_internal bool collect_when_stmt_from_file(CheckerContext *ctx, AstWhenStmt *ws) { - Operand operand = {Addressing_Invalid}; - if (!ws->is_cond_determined) { - check_expr(ctx, &operand, ws->cond); - if (operand.mode != Addressing_Invalid && !is_type_boolean(operand.type)) { - error(ws->cond, "Non-boolean condition in 'when' statement"); - } - if (operand.mode != Addressing_Constant) { - error(ws->cond, "Non-constant condition in 'when' statement"); - } - - ws->is_cond_determined = true; - ws->determined_cond = operand.value.kind == ExactValue_Bool && operand.value.value_bool; - } - - if (ws->body == nullptr || ws->body->kind != Ast_BlockStmt) { - error(ws->cond, "Invalid body for 'when' statement"); - } else { - if (ws->determined_cond) { - check_collect_entities(ctx, ws->body->BlockStmt.stmts); - return true; - } else if (ws->else_stmt) { - switch (ws->else_stmt->kind) { - case Ast_BlockStmt: - check_collect_entities(ctx, ws->else_stmt->BlockStmt.stmts); - return true; - case Ast_WhenStmt: - collect_when_stmt_from_file(ctx, &ws->else_stmt->WhenStmt); - return true; - default: - error(ws->else_stmt, "Invalid 'else' statement in 'when' statement"); - break; - } - } - } - - return false; -} - -gb_internal bool collect_file_decls_from_when_stmt(CheckerContext *ctx, AstWhenStmt *ws) { - Operand operand = {Addressing_Invalid}; - if (!ws->is_cond_determined) { - check_expr(ctx, &operand, ws->cond); - if (operand.mode != Addressing_Invalid && !is_type_boolean(operand.type)) { - error(ws->cond, "Non-boolean condition in 'when' statement"); - } - if (operand.mode != Addressing_Constant) { - error(ws->cond, "Non-constant condition in 'when' statement"); - } - - ws->is_cond_determined = true; - ws->determined_cond = operand.value.kind == ExactValue_Bool && operand.value.value_bool; - } - - if (ws->body == nullptr || ws->body->kind != Ast_BlockStmt) { - error(ws->cond, "Invalid body for 'when' statement"); - } else { - if (ws->determined_cond) { - return collect_file_decls(ctx, ws->body->BlockStmt.stmts); - } else if (ws->else_stmt) { - switch (ws->else_stmt->kind) { - case Ast_BlockStmt: - return collect_file_decls(ctx, ws->else_stmt->BlockStmt.stmts); - case Ast_WhenStmt: - return collect_file_decls_from_when_stmt(ctx, &ws->else_stmt->WhenStmt); - default: - error(ws->else_stmt, "Invalid 'else' statement in 'when' statement"); - break; - } - } - } - - return false; -} - - -gb_internal bool collect_file_decl(CheckerContext *ctx, Ast *decl) { - GB_ASSERT(ctx->scope->flags&ScopeFlag_File); - - AstFile *curr_file = ctx->scope->file; - GB_ASSERT(curr_file != nullptr); - - if (decl->state_flags & StateFlag_BeenHandled) { - return false; - } - - switch (decl->kind) { - case_ast_node(vd, ValueDecl, decl); - check_collect_value_decl(ctx, decl); - case_end; - - case_ast_node(id, ImportDecl, decl); - check_add_import_decl(ctx, decl); - case_end; - - case_ast_node(fl, ForeignImportDecl, decl); - check_add_foreign_import_decl(ctx, decl); - case_end; - - case_ast_node(fb, ForeignBlockDecl, decl); - GB_ASSERT(ctx->collect_delayed_decls); - decl->state_flags |= StateFlag_BeenHandled; - array_add(&curr_file->delayed_decls_queues[AstDelayQueue_ForeignBlock], decl); - case_end; - - case_ast_node(ws, WhenStmt, decl); - if (!ws->is_cond_determined) { - if (collect_when_stmt_from_file(ctx, ws)) { - return true; - } - - CheckerContext nctx = *ctx; - nctx.collect_delayed_decls = true; - - if (collect_file_decls_from_when_stmt(&nctx, ws)) { - return true; - } - } else { - CheckerContext nctx = *ctx; - nctx.collect_delayed_decls = true; - - if (collect_file_decls_from_when_stmt(&nctx, ws)) { - return true; - } - } - case_end; - - case_ast_node(es, ExprStmt, decl); - GB_ASSERT(ctx->collect_delayed_decls); - decl->state_flags |= StateFlag_BeenHandled; - if (es->expr->kind == Ast_CallExpr) { - ast_node(ce, CallExpr, es->expr); - if (ce->proc->kind == Ast_BasicDirective) { - array_add(&curr_file->delayed_decls_queues[AstDelayQueue_Expr], es->expr); - } - } - case_end; - } - - return false; -} - -gb_internal bool collect_file_decls(CheckerContext *ctx, Slice const &decls) { - GB_ASSERT(ctx->scope->flags&ScopeFlag_File); - - for_array(i, decls) { - if (collect_file_decl(ctx, decls[i])) { - correct_type_aliases_in_scope(ctx, ctx->scope); - return true; - } - } - correct_type_aliases_in_scope(ctx, ctx->scope); - return false; -} - gb_internal GB_COMPARE_PROC(sort_file_by_name) { AstFile const *x = *cast(AstFile const **)a; AstFile const *y = *cast(AstFile const **)b; @@ -6140,7 +5978,6 @@ gb_internal WORKER_TASK_PROC(check_collect_entities_all_worker_proc) { reset_checker_context(ctx, f, untyped); check_collect_entities(ctx, f->decls); - GB_ASSERT(ctx->collect_delayed_decls == false); add_untyped_expressions(&c->info, ctx->untyped); @@ -6205,6 +6042,8 @@ gb_internal void check_export_entities(Checker *c) { thread_pool_wait(); } +#include "checker_global_when.cpp" + gb_internal void check_import_entities(Checker *c) { TEMPORARY_ALLOCATOR_GUARD(); @@ -6261,101 +6100,45 @@ gb_internal void check_import_entities(Checker *c) { array_add(&package_order, n); } - TIME_SECTION("check_import_entities - collect file decls"); CheckerContext ctx = {}; init_checker_context(&ctx, c); + defer (destroy_checker_context(&ctx)); UntypedExprInfoMap untyped = {}; defer (map_destroy(&untyped)); - isize min_pkg_index = 0; + TIME_SECTION("check_import_entities - imports"); + // NOTE(bill): every import first, as resolving a 'when' may check declarations in any package u64 stage_start = global_import_stage_begin(); for (isize pkg_index = 0; pkg_index < package_order.count; pkg_index++) { - ImportGraphNode *node = package_order[pkg_index]; - AstPackage *pkg = node->pkg; + AstPackage *pkg = package_order[pkg_index]->pkg; pkg->order = 1+pkg_index; - for_array(i, pkg->files) { - AstFile *f = pkg->files[i]; - + for (AstFile *f : pkg->files) { reset_checker_context(&ctx, f, &untyped); - ctx.collect_delayed_decls = true; - - // Check import declarations first to simplify things for (Ast *decl : f->delayed_decls_queues[AstDelayQueue_Import]) { check_add_import_decl(&ctx, decl); } array_clear(&f->delayed_decls_queues[AstDelayQueue_Import]); - - if (collect_file_decls(&ctx, f->decls)) { - check_export_entities_in_pkg(&ctx, pkg, &untyped); - pkg_index = min_pkg_index-1; - break; - } - add_untyped_expressions(ctx.info, &untyped); } - if (pkg_index < 0) { - continue; - } - min_pkg_index = pkg_index; } - global_import_stage_end(GlobalImportStage_CollectFileDecls, stage_start); + global_import_stage_end(GlobalImportStage_Imports, stage_start); + + TIME_SECTION("check_import_entities - resolve 'when' and 'foreign' blocks"); + resolve_global_decl_sources(c, package_order); TIME_SECTION("check_import_entities - check delayed entities"); - for (isize pkg_index = 0; pkg_index < package_order.count; pkg_index++) { - ImportGraphNode *node = package_order[pkg_index]; + for (ImportGraphNode *node : package_order) { GB_ASSERT(node->scope->flags&ScopeFlag_Pkg); AstPackage *pkg = node->scope->pkg; stage_start = global_import_stage_begin(); - for_array(i, pkg->files) { - AstFile *f = pkg->files[i]; - reset_checker_context(&ctx, f, &untyped); - - for (Ast *decl : f->delayed_decls_queues[AstDelayQueue_Import]) { - check_add_import_decl(&ctx, decl); - } - array_clear(&f->delayed_decls_queues[AstDelayQueue_Import]); - add_untyped_expressions(ctx.info, &untyped); - } - global_import_stage_end(GlobalImportStage_Imports, stage_start); - - stage_start = global_import_stage_begin(); - for_array(i, pkg->files) { - AstFile *f = pkg->files[i]; - reset_checker_context(&ctx, f, &untyped); - correct_type_aliases_in_scope(&ctx, pkg->scope); - } + correct_type_aliases_in_package(&ctx, pkg); global_import_stage_end(GlobalImportStage_TypeAliases, stage_start); stage_start = global_import_stage_begin(); - for_array(i, pkg->files) { - AstFile *f = pkg->files[i]; - reset_checker_context(&ctx, f, &untyped); - - ctx.collect_delayed_decls = true; - - bool will_recheck_foreign_block = false; - for (Ast *decl : f->delayed_decls_queues[AstDelayQueue_ForeignBlock]) { - if (check_add_foreign_block_decl(&ctx, decl)) { - pkg_index -= 1; // Re-check package - will_recheck_foreign_block = true; - break; - } - } - - if (will_recheck_foreign_block) { - break; - } - - array_clear(&f->delayed_decls_queues[AstDelayQueue_ForeignBlock]); - } - global_import_stage_end(GlobalImportStage_ForeignBlocks, stage_start); - - stage_start = global_import_stage_begin(); - for_array(i, pkg->files) { - AstFile *f = pkg->files[i]; + for (AstFile *f : pkg->files) { reset_checker_context(&ctx, f, &untyped); for (Ast *expr : f->delayed_decls_queues[AstDelayQueue_Expr]) { diff --git a/src/checker.hpp b/src/checker.hpp index e51b48da3..9643fa683 100644 --- a/src/checker.hpp +++ b/src/checker.hpp @@ -564,6 +564,7 @@ struct Scope { RwMutex mutex; ScopeMap elements; PtrSet imported; + PtrMap *placeholders; // multi-map; names a global 'when' or 'foreign' block may declare, until all are resolved DeclInfo *decl_info; @@ -838,7 +839,6 @@ struct CheckerContext { u32 stmt_flags; bool in_enum_type; bool in_proc_group; - bool collect_delayed_decls; bool allow_polymorphic_types; bool disallow_polymorphic_return_types; // NOTE(zen3ger): no poly type decl in return types bool no_polymorphic_errors; diff --git a/src/checker_global_graph.cpp b/src/checker_global_graph.cpp index d639297ac..67a2c7c07 100644 --- a/src/checker_global_graph.cpp +++ b/src/checker_global_graph.cpp @@ -13,20 +13,20 @@ struct GlobalEntityTimingFrame { }; enum GlobalImportStagePart { - GlobalImportStage_CollectFileDecls, GlobalImportStage_Imports, + GlobalImportStage_Placeholders, + GlobalImportStage_DeclSources, GlobalImportStage_TypeAliases, - GlobalImportStage_ForeignBlocks, GlobalImportStage_DelayedExprs, GlobalImportStage_COUNT, }; gb_global char const *global_import_stage_names[GlobalImportStage_COUNT] = { - "collect file decls ('when')", - "delayed imports", + "imports", + "'when' and 'foreign' placeholders", + "resolve 'when' and 'foreign' blocks", "type alias correction", - "foreign blocks", "delayed expressions (#assert etc.)", }; diff --git a/src/checker_global_when.cpp b/src/checker_global_when.cpp new file mode 100644 index 000000000..2b4187b68 --- /dev/null +++ b/src/checker_global_when.cpp @@ -0,0 +1,474 @@ +// Global 'when's and 'foreign' blocks: every name one may declare is a placeholder in its scope, and the +// first lookup of a placeholder resolves them, so the order of files and declarations does not matter + +struct GlobalDeclSource { + Ast * node; // WhenStmt or ForeignBlockDecl + AstFile * file; + GlobalDeclSource *parent; + bool in_else; // within the else branch of `parent` + bool reachable; + bool reported_cycle; + EntityState state; + ForeignContext foreign_context; // of a resolved 'foreign' block +}; + +struct GlobalDeclSourceFrame { + GlobalDeclSource *source; + InternedString needs; // the placeholder being resolved for it +}; + +gb_global Array global_decl_sources; +gb_global Array global_decl_source_stack; +gb_global Array global_placeholder_scopes; +gb_global CheckerContext global_decl_source_export_ctx; +gb_global UntypedExprInfoMap global_decl_source_export_untyped; + +enum : u8 { + PlaceholderScope_File = 1<<0, + PlaceholderScope_Pkg = 1<<1, +}; + +// -1 when 'private' has a value that is not a string literal +gb_internal i32 syntactic_visibility(Array const &attributes) { + for (Ast *attr : attributes) { + if (attr->kind != Ast_Attribute) { + continue; + } + for (Ast *elem : attr->Attribute.elems) { + if (elem->kind == Ast_Ident && elem->Ident.token.string == "private") { + return EntityVisiblity_PrivateToPackage; + } + if (elem->kind == Ast_FieldValue && + elem->FieldValue.field->kind == Ast_Ident && + elem->FieldValue.field->Ident.token.string == "private") { + Ast *value = elem->FieldValue.value; + if (value != nullptr && value->tav.value.kind == ExactValue_String) { + return value->tav.value.value_string == "file" ? EntityVisiblity_PrivateToFile : EntityVisiblity_PrivateToPackage; + } + return -1; + } + } + } + return EntityVisiblity_Public; +} + +gb_internal bool has_syntactic_attribute(Array const &attributes, String const &name) { + for (Ast *attr : attributes) { + if (attr->kind != Ast_Attribute) { + continue; + } + for (Ast *elem : attr->Attribute.elems) { + Ast *field = elem->kind == Ast_FieldValue ? elem->FieldValue.field : elem; + if (field->kind == Ast_Ident && field->Ident.token.string == name) { + return true; + } + } + } + return false; +} + +gb_internal void add_placeholder(Scope *s, InternedString name, GlobalDeclSource *src) { + if (name.value == 0 || name.is_blank()) { + return; + } + if (s->placeholders == nullptr) { + s->placeholders = permanent_alloc_item>(); + map_init(s->placeholders); + array_add(&global_placeholder_scopes, s); + } + u64 key = name.value; + for (auto *e = multi_map_find_first(s->placeholders, key); e != nullptr; e = multi_map_find_next(s->placeholders, e)) { + if (e->value == src) { + return; + } + } + multi_map_insert(s->placeholders, key, src); +} + +gb_internal void add_placeholders(AstFile *f, u8 scopes, InternedString name, GlobalDeclSource *src) { + if (scopes & PlaceholderScope_File) { + add_placeholder(f->scope, name, src); + } + if (scopes & PlaceholderScope_Pkg) { + add_placeholder(f->pkg->scope, name, src); + } +} + +gb_internal GlobalDeclSource *add_global_decl_source(Ast *node, AstFile *f, GlobalDeclSource *parent, bool in_else) { + GlobalDeclSource *src = permanent_alloc_item(); + src->node = node; + src->file = f; + src->parent = parent; + src->in_else = in_else; + src->reachable = true; + src->state = EntityState_Unresolved; + array_add(&global_decl_sources, src); + return src; +} + +gb_internal void scan_global_decl_sources(AstFile *f, Slice const &stmts, GlobalDeclSource *owner, bool in_else, i32 foreign_visibility); + +gb_internal void scan_global_when_stmt(AstFile *f, Ast *node, GlobalDeclSource *parent, bool in_else, i32 foreign_visibility) { + ast_node(ws, WhenStmt, node); + GlobalDeclSource *src = add_global_decl_source(node, f, parent, in_else); + if (ws->body != nullptr && ws->body->kind == Ast_BlockStmt) { + scan_global_decl_sources(f, ws->body->BlockStmt.stmts, src, false, foreign_visibility); + } + if (ws->else_stmt != nullptr) { + switch (ws->else_stmt->kind) { + case Ast_BlockStmt: + scan_global_decl_sources(f, ws->else_stmt->BlockStmt.stmts, src, true, foreign_visibility); + break; + case Ast_WhenStmt: + scan_global_when_stmt(f, ws->else_stmt, src, true, foreign_visibility); + break; + } + } +} + +gb_internal void scan_global_decl_sources(AstFile *f, Slice const &stmts, GlobalDeclSource *owner, bool in_else, i32 foreign_visibility) { + // NOTE(bill): `owner == nullptr` is the file scope itself, whose other declarations are already collected + for (Ast *decl : stmts) { + switch (decl->kind) { + case_ast_node(vd, ValueDecl, decl); + if (owner == nullptr) { + break; + } + i32 visibility = syntactic_visibility(vd->attributes); + if (visibility == EntityVisiblity_Public) { + visibility = foreign_visibility; + } + if (visibility == EntityVisiblity_Public && (f->flags & AstFile_IsPrivateFile)) { + visibility = EntityVisiblity_PrivateToFile; + } + u8 scopes = PlaceholderScope_Pkg; + if (visibility == EntityVisiblity_PrivateToFile) { + scopes = PlaceholderScope_File; + } else if (visibility < 0) { + scopes = PlaceholderScope_File|PlaceholderScope_Pkg; + } + for (Ast *name : vd->names) { + if (name->kind == Ast_Ident) { + add_placeholders(f, scopes, name->Ident.interned, owner); + } + } + case_end; + + case_ast_node(fl, ForeignImportDecl, decl); + if (owner == nullptr) { + break; + } + String library_name = fl->library_name.string; + if (library_name.len == 0 && fl->fullpaths.count != 0) { + library_name = path_to_entity_name(fl->library_name.string, fl->fullpaths[0]); + } + if (library_name.len != 0) { + u8 scopes = has_syntactic_attribute(fl->attributes, str_lit("export")) ? PlaceholderScope_Pkg : PlaceholderScope_File; + add_placeholders(f, scopes, string_interner_insert(library_name), owner); + } + case_end; + + case_ast_node(fb, ForeignBlockDecl, decl); + GlobalDeclSource *src = add_global_decl_source(decl, f, owner, in_else); + if (fb->body != nullptr && fb->body->kind == Ast_BlockStmt) { + scan_global_decl_sources(f, fb->body->BlockStmt.stmts, src, false, syntactic_visibility(fb->attributes)); + } + case_end; + + case_ast_node(ws, WhenStmt, decl); + scan_global_when_stmt(f, decl, owner, in_else, foreign_visibility); + case_end; + + case_ast_node(es, ExprStmt, decl); + if (owner == nullptr && es->expr->kind == Ast_CallExpr && + es->expr->CallExpr.proc->kind == Ast_BasicDirective && + (decl->state_flags & StateFlag_BeenHandled) == 0) { + decl->state_flags |= StateFlag_BeenHandled; + array_add(&f->delayed_decls_queues[AstDelayQueue_Expr], es->expr); + } + case_end; + } + } +} + +gb_internal bool is_global_decl_source_in_when(GlobalDeclSource *src) { + for (; src != nullptr; src = src->parent) { + if (src->node->kind == Ast_WhenStmt) { + return true; + } + } + return false; +} + +gb_internal Slice global_decl_source_taken_stmts(GlobalDeclSource *src) { + if (src->node->kind == Ast_ForeignBlockDecl) { + Ast *body = src->node->ForeignBlockDecl.body; + if (body != nullptr && body->kind == Ast_BlockStmt) { + return body->BlockStmt.stmts; + } + return {}; + } + ast_node(ws, WhenStmt, src->node); + if (ws->determined_cond) { + if (ws->body != nullptr && ws->body->kind == Ast_BlockStmt) { + return ws->body->BlockStmt.stmts; + } + } else if (ws->else_stmt != nullptr && ws->else_stmt->kind == Ast_BlockStmt) { + return ws->else_stmt->BlockStmt.stmts; + } + return {}; +} + +gb_internal void collect_global_decl_source_stmts(CheckerContext *ctx, Slice const &stmts) { + AstFile *f = ctx->file; + for (Ast *decl : stmts) { + if (decl->kind == Ast_ValueDecl) { + check_collect_value_decl(ctx, decl); + } + } + check_export_entities_in_pkg(&global_decl_source_export_ctx, f->pkg, &global_decl_source_export_untyped); + + // NOTE(bill): after the value declarations, as their attributes are evaluated + for (Ast *decl : stmts) { + switch (decl->kind) { + case_ast_node(fl, ForeignImportDecl, decl); + check_add_foreign_import_decl(ctx, decl); + case_end; + + case_ast_node(es, ExprStmt, decl); + if (es->expr->kind == Ast_CallExpr && es->expr->CallExpr.proc->kind == Ast_BasicDirective && + (decl->state_flags & StateFlag_BeenHandled) == 0) { + decl->state_flags |= StateFlag_BeenHandled; + array_add(&f->delayed_decls_queues[AstDelayQueue_Expr], es->expr); + } + case_end; + } + } +} + +gb_internal Token global_decl_source_token(GlobalDeclSource *src) { + if (src->node->kind == Ast_WhenStmt) { + return src->node->WhenStmt.token; + } + return src->node->ForeignBlockDecl.token; +} + +gb_internal void report_global_decl_source_cycle(GlobalDeclSource *src, InternedString needed) { + if (src->reported_cycle) { + return; + } + src->reported_cycle = true; + + isize start = 0; + for (isize i = global_decl_source_stack.count-1; i >= 0; i--) { + if (global_decl_source_stack[i].source == src) { + start = i; + break; + } + } + + ERROR_BLOCK(); + Token token = global_decl_source_token(src); + error(token, "Cyclic dependency between global '%.*s' declarations", LIT(token.string)); + for (isize i = start; i < global_decl_source_stack.count; i++) { + Token t = global_decl_source_token(global_decl_source_stack[i].source); + InternedString name = i+1 < global_decl_source_stack.count ? global_decl_source_stack[i].needs : needed; + error_line("\t'%.*s' at %s needs '%s', which may be declared by\n", LIT(t.string), token_pos_to_string(t.pos), name.cstring()); + } + error_line("\t'%.*s' at %s\n", LIT(token.string), token_pos_to_string(token.pos)); +} + +gb_internal void resolve_global_decl_source(GlobalDeclSource *src, InternedString needed) { + if (src->state == EntityState_Resolved) { + return; + } + if (src->state == EntityState_InProgress) { + report_global_decl_source_cycle(src, needed); + return; + } + + GlobalDeclSource *foreign_block = nullptr; + if (src->parent != nullptr) { + GlobalDeclSource *parent = src->parent; + resolve_global_decl_source(parent, needed); + if (parent->state != EntityState_Resolved) { + return; + } + bool reachable = parent->reachable; + if (parent->node->kind == Ast_WhenStmt) { + reachable = reachable && parent->node->WhenStmt.determined_cond != src->in_else; + } + if (!reachable) { + src->reachable = false; + src->state = EntityState_Resolved; + return; + } + for (GlobalDeclSource *p = parent; p != nullptr; p = p->parent) { + if (p->node->kind == Ast_ForeignBlockDecl) { + foreign_block = p; + break; + } + } + } + + src->state = EntityState_InProgress; + array_add(&global_decl_source_stack, GlobalDeclSourceFrame{src, {}}); + + CheckerContext ctx = {}; + init_checker_context(&ctx, global_checker_ptr.load(std::memory_order_relaxed)); + UntypedExprInfoMap untyped = {}; + reset_checker_context(&ctx, src->file, &untyped); + if (foreign_block != nullptr) { + ctx.foreign_context = foreign_block->foreign_context; + } + + if (src->node->kind == Ast_WhenStmt) { + ast_node(ws, WhenStmt, src->node); + Operand operand = {Addressing_Invalid}; + check_expr(&ctx, &operand, ws->cond); + if (operand.mode != Addressing_Invalid && !is_type_boolean(operand.type)) { + error(ws->cond, "Non-boolean condition in 'when' statement"); + } + if (operand.mode != Addressing_Constant) { + error(ws->cond, "Non-constant condition in 'when' statement"); + } + ws->is_cond_determined = true; + ws->determined_cond = operand.value.kind == ExactValue_Bool && operand.value.value_bool; + if (ws->body == nullptr || ws->body->kind != Ast_BlockStmt) { + error(ws->cond, "Invalid body for 'when' statement"); + } else if (ws->else_stmt != nullptr && ws->else_stmt->kind != Ast_BlockStmt && ws->else_stmt->kind != Ast_WhenStmt) { + error(ws->else_stmt, "Invalid 'else' statement in 'when' statement"); + } + } else { + ast_node(fb, ForeignBlockDecl, src->node); + if (fb->foreign_library->kind == Ast_Ident) { + ctx.foreign_context.curr_library = fb->foreign_library; + } else { + error(fb->foreign_library, "Foreign block name must be an identifier or 'export'"); + ctx.foreign_context.curr_library = nullptr; + } + check_decl_attributes(&ctx, fb->attributes, foreign_block_decl_attribute, nullptr); + src->foreign_context = ctx.foreign_context; + } + + // NOTE: resolved before its declarations are collected, which evaluates the attributes of 'foreign import's + src->state = EntityState_Resolved; + array_pop(&global_decl_source_stack); + + collect_global_decl_source_stmts(&ctx, global_decl_source_taken_stmts(src)); + + add_untyped_expressions(ctx.info, &untyped); + map_destroy(&untyped); + destroy_checker_context(&ctx); +} + +gb_internal Entity *force_scope_placeholders(Scope *s, InternedString name, u32 hash) { + PtrMap *m = s->placeholders; + bool forced = false; + for (auto *e = multi_map_find_first(m, cast(u64)name.value); e != nullptr; e = multi_map_find_next(m, e)) { + GlobalDeclSource *src = e->value; + if (src->state != EntityState_Resolved) { + if (global_decl_source_stack.count > 0) { + global_decl_source_stack[global_decl_source_stack.count-1].needs = name; + } + resolve_global_decl_source(src, name); + forced = true; + } + } + if (!forced) { + return nullptr; + } + rw_mutex_shared_lock(&s->mutex); + Entity *found = scope_map_get(&s->elements, name, hash); + rw_mutex_shared_unlock(&s->mutex); + return found; +} + +gb_internal void check_vet_when_shadowing_entity(Entity *e) { + if (e == nullptr || e->scope == nullptr || (e->scope->flags & ScopeFlag_File) == 0) { + return; + } + InternedString name = entity_interned_name(e); + u32 hash = e->interned_name_hash.load(std::memory_order_relaxed); + Scope *outer = e->scope->parent; + if (scope_map_get(&e->scope->elements, name, hash) != e) { + outer = outer->parent; // in the package scope + } + if (outer == nullptr) { + return; + } + Entity *shadowed = scope_lookup(outer, name, hash); + if (shadowed == nullptr || shadowed == e) { + return; + } + if (shadowed->scope == builtin_pkg->scope) { + error(e->token, "Declaration of '%.*s' within a global 'when' shadows the builtin '%.*s'", LIT(e->token.string), LIT(e->token.string)); + } else { + error(e->token, "Declaration of '%.*s' within a global 'when' shadows the declaration at %s", LIT(e->token.string), token_pos_to_string(shadowed->token.pos)); + } +} + +gb_internal void check_vet_when_shadowing(void) { + for (GlobalDeclSource *src : global_decl_sources) { + if (!src->reachable || src->state != EntityState_Resolved) { + continue; + } + if ((ast_file_vet_flags(src->file) & VetFlag_WhenShadowing) == 0 || !is_global_decl_source_in_when(src)) { + continue; + } + for (Ast *decl : global_decl_source_taken_stmts(src)) { + if (decl->kind == Ast_ValueDecl) { + for (Ast *name : decl->ValueDecl.names) { + if (name->kind == Ast_Ident) { + check_vet_when_shadowing_entity(name->Ident.entity.load()); + } + } + } else if (decl->kind == Ast_ForeignImportDecl) { + Token token = decl->ForeignImportDecl.library_name; + InternedString name = string_interner_insert(token.string); + for (Scope *s = src->file->scope; s != nullptr && s != builtin_pkg->scope; s = s->parent) { + Entity *e = scope_map_get(&s->elements, name, name.hash()); + if (e != nullptr && e->kind == Entity_LibraryName && e->LibraryName.decl == decl) { + check_vet_when_shadowing_entity(e); + break; + } + } + } + } + } +} + +// Placeholders for every file, then every source resolved in package, file and source order, which +// only matters for which errors are reported +gb_internal void resolve_global_decl_sources(Checker *c, Array const &package_order) { + array_init(&global_decl_sources, heap_allocator()); + array_init(&global_decl_source_stack, heap_allocator()); + array_init(&global_placeholder_scopes, heap_allocator()); + init_checker_context(&global_decl_source_export_ctx, c); + defer (destroy_checker_context(&global_decl_source_export_ctx)); + + u64 stage_start = global_import_stage_begin(); + for (ImportGraphNode *node : package_order) { + for (AstFile *f : node->pkg->files) { + scan_global_decl_sources(f, f->decls, nullptr, false, EntityVisiblity_Public); + } + } + global_import_stage_end(GlobalImportStage_Placeholders, stage_start); + + stage_start = global_import_stage_begin(); + for (GlobalDeclSource *src : global_decl_sources) { + resolve_global_decl_source(src, {}); + } + GB_ASSERT(global_decl_source_stack.count == 0); + + for (Scope *s : global_placeholder_scopes) { + map_destroy(s->placeholders); + s->placeholders = nullptr; + } + array_clear(&global_placeholder_scopes); + + check_vet_when_shadowing(); + global_import_stage_end(GlobalImportStage_DeclSources, stage_start); + + map_destroy(&global_decl_source_export_untyped); +} diff --git a/src/main.cpp b/src/main.cpp index 6f0308cfb..5a3f4e3da 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -460,6 +460,7 @@ enum BuildFlagKind { BuildFlag_VetSemicolon, BuildFlag_VetCast, BuildFlag_VetTabs, + BuildFlag_VetWhenShadowing, BuildFlag_VetPackages, BuildFlag_CustomAttribute, @@ -725,6 +726,7 @@ gb_internal bool parse_build_flags(Array args) { add_flag(&build_flags, BuildFlag_VetSemicolon, str_lit("vet-semicolon"), BuildFlagParam_None, Command__does_check); add_flag(&build_flags, BuildFlag_VetCast, str_lit("vet-cast"), BuildFlagParam_None, Command__does_check); add_flag(&build_flags, BuildFlag_VetTabs, str_lit("vet-tabs"), BuildFlagParam_None, Command__does_check); + add_flag(&build_flags, BuildFlag_VetWhenShadowing, str_lit("vet-when-shadowing"), BuildFlagParam_None, Command__does_check); add_flag(&build_flags, BuildFlag_VetPackages, str_lit("vet-packages"), BuildFlagParam_String, Command__does_check); add_flag(&build_flags, BuildFlag_CustomAttribute, str_lit("custom-attribute"), BuildFlagParam_String, Command__does_check, true); @@ -1493,6 +1495,7 @@ gb_internal bool parse_build_flags(Array args) { case BuildFlag_VetSemicolon: build_context.vet_flags |= VetFlag_Semicolon; break; case BuildFlag_VetCast: build_context.vet_flags |= VetFlag_Cast; break; case BuildFlag_VetTabs: build_context.vet_flags |= VetFlag_Tabs; break; + case BuildFlag_VetWhenShadowing: build_context.vet_flags |= VetFlag_WhenShadowing; break; case BuildFlag_VetUnusedProcedures: build_context.vet_flags |= VetFlag_UnusedProcedures; break; case BuildFlag_VetPackages: @@ -3408,6 +3411,7 @@ gb_internal int print_show_help(String const arg0, String command, String option print_usage_line(3, "-vet-unused-imports"); print_usage_line(3, "-vet-shadowing"); print_usage_line(3, "-vet-using-stmt"); + print_usage_line(3, "-vet-when-shadowing"); } if (print_flag("-vet-cast")) { @@ -3467,6 +3471,10 @@ gb_internal int print_show_help(String const arg0, String command, String option print_usage_line(2, "Checks for the use of 'using' as a statement."); print_usage_line(2, "'using' is considered bad practice outside of immediate refactoring."); } + + if (print_flag("-vet-when-shadowing")) { + print_usage_line(2, "Checks for declarations within a global 'when' that shadow a builtin or package-level name."); + } } if (check) { diff --git a/src/parser.cpp b/src/parser.cpp index 8f4cc99ec..3f6578ab4 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -7243,6 +7243,7 @@ gb_internal u64 parse_vet_tag(Token token_for_pos, String s, u64 base_vet_flags) error_line("\tcast\n"); error_line("\ttabs\n"); error_line("\texplicit-allocators\n"); + error_line("\twhen-shadowing\n"); return vet_flags; } } diff --git a/src/parser.hpp b/src/parser.hpp index 08811c37a..f9d8da6af 100644 --- a/src/parser.hpp +++ b/src/parser.hpp @@ -101,7 +101,6 @@ enum AstFileFlag : u32 { enum AstDelayQueueKind { AstDelayQueue_Import, AstDelayQueue_Expr, - AstDelayQueue_ForeignBlock, AstDelayQueue_COUNT, }; From 98e9aba1fa404381cb1175442e05eb56db54e34a Mon Sep 17 00:00:00 2001 From: gingerBill Date: Thu, 1 Oct 2026 16:28:45 +0100 Subject: [PATCH 02/17] Add loads of global when tests --- tests/issues/run.bat | 6 ++++ tests/issues/run.sh | 21 +++++++++++ .../test_issue_const_array_broadcast.odin | 35 +++++++++++++++++++ .../issues/test_issue_global_when_cycle.odin | 14 ++++++++ .../issues/test_issue_global_when_order.odin | 35 +++++++++++++++++++ .../test_issue_global_when_shadowing.odin | 6 ++++ 6 files changed, 117 insertions(+) create mode 100644 tests/issues/test_issue_global_when_cycle.odin create mode 100644 tests/issues/test_issue_global_when_order.odin create mode 100644 tests/issues/test_issue_global_when_shadowing.odin diff --git a/tests/issues/run.bat b/tests/issues/run.bat index dd101297e..991a87d7f 100644 --- a/tests/issues/run.bat +++ b/tests/issues/run.bat @@ -49,7 +49,13 @@ set COMMON=-define:ODIN_TEST_FANCY=false -file -vet -strict-style -ignore-unused ..\..\..\odin test ..\test_issue_bool_to_be_conversion.odin %COMMON% || exit /b ..\..\..\odin test ..\test_issue_bool_comparison_truthiness.odin %COMMON% || exit /b ..\..\..\odin test ..\test_issue_const_array_broadcast.odin %COMMON% || exit /b +..\..\..\odin test ..\test_issue_decl_order.odin %COMMON% || exit /b +..\..\..\odin test ..\test_issue_distinct_constraint.odin %COMMON% || exit /b +..\..\..\odin check ..\test_issue_ambiguous_union_literal.odin %COMMON% 2>&1 | find /c "Error:" | findstr /x "1" || exit /b ..\..\..\odin test ..\test_issue_proc_constant_instantiation.odin %COMMON% || exit /b +..\..\..\odin test ..\test_issue_global_when_order.odin %COMMON% || exit /b +..\..\..\odin check ..\test_issue_global_when_cycle.odin -no-entry-point %COMMON% 2>&1 | find /c "Cyclic dependency between global" | findstr /x "3" || exit /b +..\..\..\odin check ..\test_issue_global_when_shadowing.odin -no-entry-point %COMMON% 2>&1 | find /c "within a global" | findstr /x "2" || exit /b ..\..\..\odin check ..\test_issue_7336.odin -no-entry-point %COMMON% || exit /b ..\..\..\odin check ..\test_issue_ellipsis_type_call.odin -no-entry-point %COMMON% 2>&1 | find /c "Error:" | findstr /x "10" || exit /b ..\..\..\odin check ..\test_issue_foreign_redeclaration.odin -no-entry-point %COMMON% || exit /b diff --git a/tests/issues/run.sh b/tests/issues/run.sh index 5508b07cf..41f108d4d 100755 --- a/tests/issues/run.sh +++ b/tests/issues/run.sh @@ -119,8 +119,29 @@ $ODIN check ../test_issue_7260.odin -no-entry-point $COMMON_CHECK $ODIN test ../test_issue_bool_to_be_conversion.odin $COMMON $ODIN test ../test_issue_bool_comparison_truthiness.odin $COMMON $ODIN test ../test_issue_const_array_broadcast.odin $COMMON +$ODIN test ../test_issue_decl_order.odin $COMMON +$ODIN test ../test_issue_distinct_constraint.odin $COMMON +if [[ $($ODIN check ../test_issue_ambiguous_union_literal.odin $COMMON_CHECK 2>&1 >/dev/null | grep -c "Error:") -eq 1 ]]; then + echo "SUCCESSFUL 1/1" +else + echo "SUCCESSFUL 0/1" + exit 1 +fi $ODIN test ../test_issue_proc_constant_instantiation.odin $COMMON $ODIN test ../test_issue_swizzle_multi_assign.odin $COMMON +$ODIN test ../test_issue_global_when_order.odin $COMMON +if [[ $($ODIN check ../test_issue_global_when_cycle.odin -no-entry-point $COMMON_CHECK 2>&1 >/dev/null | grep -c "Cyclic dependency between global") -eq 3 ]]; then + echo "SUCCESSFUL 1/1" +else + echo "SUCCESSFUL 0/1" + exit 1 +fi +if [[ $($ODIN check ../test_issue_global_when_shadowing.odin -no-entry-point $COMMON_CHECK 2>&1 >/dev/null | grep -c "within a global 'when' shadows") -eq 2 ]]; then + echo "SUCCESSFUL 1/1" +else + echo "SUCCESSFUL 0/1" + exit 1 +fi $ODIN check ../test_issue_foreign_redeclaration.odin -no-entry-point $COMMON_CHECK if [[ $($ODIN check ../test_issue_foreign_redeclaration_mismatch.odin -no-entry-point $COMMON_CHECK 2>&1 >/dev/null | grep -c "Error:") -eq 1 ]]; then diff --git a/tests/issues/test_issue_const_array_broadcast.odin b/tests/issues/test_issue_const_array_broadcast.odin index 5960e2f0c..b32ed12a8 100644 --- a/tests/issues/test_issue_const_array_broadcast.odin +++ b/tests/issues/test_issue_const_array_broadcast.odin @@ -67,6 +67,41 @@ const_array_broadcast_union :: proc(t: ^testing.T) { testing.expect_value(t, union_local, union_global) } +// A named untyped constant against a union hint, broadcast into an array variant, lost its +// constant value and the backend then tried to load the constant entity as a variable. + +@(private="file") +Target :: union { + []Item, + [2]u32, +} + +@(private="file") +TARGET_SIZE :: 8192 + +@(private="file") +take_target :: proc(target: Target, count: u32) -> Target { + return target +} + +@(test) +const_array_broadcast_into_union_variant :: proc(t: ^testing.T) { + expected := [2]u32{TARGET_SIZE, TARGET_SIZE} + + assigned: Target = TARGET_SIZE + testing.expect_value(t, assigned.([2]u32), expected) + + named := take_target(target = TARGET_SIZE, count = 3) + testing.expect_value(t, named.([2]u32), expected) + + positional := take_target(TARGET_SIZE*2, 3) + testing.expect_value(t, positional.([2]u32), [2]u32{2*TARGET_SIZE, 2*TARGET_SIZE}) + + elems := [2]Target{TARGET_SIZE, {}} + testing.expect_value(t, elems[0].([2]u32), expected) + testing.expect(t, elems[1] == nil, "the second element should be nil") +} + @(test) const_array_literals_unchanged :: proc(t: ^testing.T) { // a literal for the array's own type is not a broadcast diff --git a/tests/issues/test_issue_global_when_cycle.odin b/tests/issues/test_issue_global_when_cycle.odin new file mode 100644 index 000000000..77f83a028 --- /dev/null +++ b/tests/issues/test_issue_global_when_cycle.odin @@ -0,0 +1,14 @@ +// Each of these global 'when's needs a name it may declare itself, so all three are cycles +package test_issues + +// each 'when' needs a name the other may declare +when size_of(int) == 8 { A :: 1 } +when A == 1 { int :: i32 } + +// a 'when' needing a name it may declare +when B == 1 { B :: 1 } + +// or one declared by a 'when' within it +when C == 1 { + when true { C :: 1 } +} diff --git a/tests/issues/test_issue_global_when_order.odin b/tests/issues/test_issue_global_when_order.odin new file mode 100644 index 000000000..2f58aa844 --- /dev/null +++ b/tests/issues/test_issue_global_when_order.odin @@ -0,0 +1,35 @@ +// Global 'when's are resolved when a lookup needs a name they may declare, whatever the source order +package test_issues + +import "core:testing" + +// needs a name declared by a later 'when' +when HAS_FOO { X :: 1 } else { X :: 2 } +when true { HAS_FOO :: true } + +// nested 'when's and 'else when' chains +when A_ENABLED { + when B_VALUE == 3 { NESTED :: "three" } else { NESTED :: "other" } +} +when false { B_VALUE :: 1 } else when true { B_VALUE :: 3 } else { B_VALUE :: 4 } +A_ENABLED :: B_VALUE > 0 + +// file private names +when PRIV == 5 { FROM_PRIV :: true } else { FROM_PRIV :: false } +when true { @(private="file") PRIV :: 5 } + +// a 'when' within a 'foreign' block +when size_of(type_of(foreign_proc)) == size_of(rawptr) { FOREIGN_OK :: true } else { FOREIGN_OK :: false } +foreign { + when true { + foreign_proc :: proc "c" () --- + } +} + +@(test) +test_global_when_order :: proc(t: ^testing.T) { + testing.expect_value(t, X, 1) + testing.expect_value(t, NESTED, "three") + testing.expect_value(t, FROM_PRIV, true) + testing.expect_value(t, FOREIGN_OK, true) +} diff --git a/tests/issues/test_issue_global_when_shadowing.odin b/tests/issues/test_issue_global_when_shadowing.odin new file mode 100644 index 000000000..eb5797429 --- /dev/null +++ b/tests/issues/test_issue_global_when_shadowing.odin @@ -0,0 +1,6 @@ +// -vet-when-shadowing, enabled by -vet: a global 'when' declaring a builtin name +package test_issues + +when true { uint :: u32 } +when true { @(private="file") byte :: u16 } +when true { not_shadowing :: 1 } From 379261331ff6865249dfe4142330cd205e12b6b2 Mon Sep 17 00:00:00 2001 From: gingerBill Date: Thu, 1 Oct 2026 16:29:04 +0100 Subject: [PATCH 03/17] Add more tests --- tests/issues/test_issue_decl_order.odin | 80 +++++++++++++++++++ .../test_issue_distinct_constraint.odin | 21 +++++ 2 files changed, 101 insertions(+) create mode 100644 tests/issues/test_issue_decl_order.odin create mode 100644 tests/issues/test_issue_distinct_constraint.odin diff --git a/tests/issues/test_issue_decl_order.odin b/tests/issues/test_issue_decl_order.odin new file mode 100644 index 000000000..0f27a72b2 --- /dev/null +++ b/tests/issues/test_issue_decl_order.odin @@ -0,0 +1,80 @@ +// Global declarations whose result used to depend on which declaration was checked first. +// Each case puts the declaration that used to break first in source order. +package test_issues + +import "core:testing" + +// A global's initializer reads other globals, directly and through procedures, so they must be +// initialized in dependency order rather than source order. +@(private="file") init_a := init_b + 1 +@(private="file") init_b := init_c * 2 +@(private="file") init_c := init_f() +@(private="file") init_f :: proc "contextless" () -> int { return 3 } + +@(private="file") init_v := init_p1() +@(private="file") init_p1 :: proc "contextless" () -> int { return init_p2() } +@(private="file") init_p2 :: proc "contextless" () -> int { return init_p3() } +@(private="file") init_p3 :: proc "contextless" () -> int { return init_w } +@(private="file") init_w := init_g() +@(private="file") init_g :: proc "contextless" () -> int { return 42 } + +@(test) +global_init_order :: proc(t: ^testing.T) { + testing.expect_value(t, init_a, 7) + testing.expect_value(t, init_b, 6) + testing.expect_value(t, init_c, 3) + testing.expect_value(t, init_v, 42) + testing.expect_value(t, init_w, 42) +} + +// An alias of an alias of a procedure group, used before the inner alias is checked. +@(private="file") group_a :: proc(x: int) -> int { return x } +@(private="file") group_b :: proc(x: string) -> int { return len(x) } +@(private="file") group :: proc{group_a, group_b} +@(private="file") outer_alias :: inner_alias +@(private="file") inner_alias :: group + +@(test) +alias_of_procedure_group_alias :: proc(t: ^testing.T) { + testing.expect_value(t, outer_alias(3), 3) + testing.expect_value(t, outer_alias("hi"), 2) +} + +// A type alias in a cycle through a pointer, checked before the union that closes the cycle. +@(private="file") Map :: []Map_Entry +@(private="file") Map_Entry :: struct { key: Value } +@(private="file") Value :: union { int, ^Map } + +@(test) +alias_in_pointer_cycle :: proc(t: ^testing.T) { + m := new(Map) + defer free(m) + v: Value = m + _, ok := v.(^Map) + testing.expect(t, ok, "the union should hold the ^Map variant") +} + +// A type still being checked, used as a polymorphic record argument behind a pointer. +@(private="file") Box :: struct($T: typeid) { value: T } +@(private="file") Queued :: distinct [dynamic]^Future +@(private="file") Queue_Context :: struct { queued: ^Box(Queued) } +@(private="file") Future :: struct { ctx: ^Queue_Context, id: int } + +@(test) +in_progress_type_as_polymorphic_argument :: proc(t: ^testing.T) { + b: Box(Queued) + c := Queue_Context{queued = &b} + testing.expect_value(t, len(c.queued.value), 0) +} + +// A foreign library alias declared after the foreign block that uses it. +foreign lib { + @(link_name="odin_test_issue_decl_order_never_called") + never_called :: proc "c" () --- +} +when ODIN_OS == .Windows { + foreign import lib_ "system:kernel32.lib" +} else { + foreign import lib_ "system:c" +} +lib :: lib_ diff --git a/tests/issues/test_issue_distinct_constraint.odin b/tests/issues/test_issue_distinct_constraint.odin new file mode 100644 index 000000000..f702a5781 --- /dev/null +++ b/tests/issues/test_issue_distinct_constraint.odin @@ -0,0 +1,21 @@ +// A `distinct` copy of a record satisfies a `$T/Record` constraint, as it shares its base record. +package test_issues + +import "core:testing" + +@(private="file") Foo :: struct { a: int } +@(private="file") Bar :: distinct Foo +@(private="file") Baz :: distinct Bar +@(private="file") U :: union { int, f32 } +@(private="file") DU :: distinct U + +@(private="file") take_foo :: proc(foo: $T/Foo) -> int { return foo.a } +@(private="file") take_u :: proc(u: $T/U) -> bool { return u != nil } + +@(test) +distinct_record_satisfies_constraint :: proc(t: ^testing.T) { + testing.expect_value(t, take_foo(Foo{1}), 1) + testing.expect_value(t, take_foo(Bar{2}), 2) + testing.expect_value(t, take_foo(Baz{3}), 3) + testing.expect(t, take_u(DU(2)), "a distinct union satisfies its constraint") +} From 84f06ee6a814d323f0efcd7f78d3d65a9d37fc89 Mon Sep 17 00:00:00 2001 From: gingerBill Date: Thu, 1 Oct 2026 16:30:48 +0100 Subject: [PATCH 04/17] Add test_issue_ambiguous_union_literal.odin --- tests/issues/test_issue_ambiguous_union_literal.odin | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 tests/issues/test_issue_ambiguous_union_literal.odin diff --git a/tests/issues/test_issue_ambiguous_union_literal.odin b/tests/issues/test_issue_ambiguous_union_literal.odin new file mode 100644 index 000000000..1b2e11040 --- /dev/null +++ b/tests/issues/test_issue_ambiguous_union_literal.odin @@ -0,0 +1,12 @@ +// An untyped compound literal that matches two union variants is one ambiguity error. Trying the +// overloads of `append` muted the error but not its follow-up lines, which crashed the error system. +package test_issues + +A :: struct { x: int } +B :: struct { x: int } +U :: union { A, B } + +main :: proc() { + d: [dynamic]U + append(&d, {x = 1}) +} From 63bd49c88bc04fc5d0523a5e8e8d66e11ba54ef1 Mon Sep 17 00:00:00 2001 From: gingerBill Date: Thu, 1 Oct 2026 17:28:38 +0100 Subject: [PATCH 05/17] Add `-internal-check-global-edges`; Check global entities in dependency-ordered groups of a syntactic graph --- src/build_settings.cpp | 1 + src/check_decl.cpp | 1 + src/checker.cpp | 50 +-- src/checker.hpp | 1 + src/checker_global_groups.cpp | 769 ++++++++++++++++++++++++++++++++++ src/main.cpp | 5 + 6 files changed, 780 insertions(+), 47 deletions(-) create mode 100644 src/checker_global_groups.cpp diff --git a/src/build_settings.cpp b/src/build_settings.cpp index 7f4da5140..7add7d37e 100644 --- a/src/build_settings.cpp +++ b/src/build_settings.cpp @@ -603,6 +603,7 @@ struct BuildContext { bool internal_llvm_no_sroa; bool internal_global_entity_graph; u64 internal_shuffle_global_entities; // seed, 0 = no shuffle + bool internal_check_global_edges; bool enable_rvo; diff --git a/src/check_decl.cpp b/src/check_decl.cpp index 472daaf6f..fd9672f21 100644 --- a/src/check_decl.cpp +++ b/src/check_decl.cpp @@ -2183,6 +2183,7 @@ gb_internal void check_entity_decl(CheckerContext *ctx, Entity *e, DeclInfo *d, c.scope->flags &= ~ScopeFlag_ContextDefined; } + global_group_check_edge(ctx, e); e->parent_proc_decl = c.curr_proc_decl; e->state = EntityState_InProgress; diff --git a/src/checker.cpp b/src/checker.cpp index 27ef4b147..fa454f404 100644 --- a/src/checker.cpp +++ b/src/checker.cpp @@ -5339,53 +5339,7 @@ gb_internal void check_single_global_entity(Checker *c, Entity *e, DeclInfo *d) check_entity_decl(ctx, e, d, nullptr); } -// for `-internal-shuffle-global-entities` -// visit globals in a seeded random order, so that any result depending on the visiting order shows up without needing threads -gb_internal void shuffle_global_entities(Array *entities, u64 seed) { - if (seed == 0) { - return; - } - u64 state = seed; - for (isize i = entities->count-1; i > 0; i--) { - state = state*6364136223846793005ull + 1442695040888963407ull; - isize j = cast(isize)((state >> 33) % cast(u64)(i+1)); - Entity *tmp = (*entities)[i]; - (*entities)[i] = (*entities)[j]; - (*entities)[j] = tmp; - } -} - -gb_internal void check_all_global_entities(Checker *c) { - in_single_threaded_checker_stage.store(true, std::memory_order_relaxed); - - auto order = array_clone(heap_allocator(), c->info.entities); - defer (array_free(&order)); - shuffle_global_entities(&order, build_context.internal_shuffle_global_entities); - - // NOTE(bill): This must be single threaded - // Don't bother trying - for (Entity *e : order) { - GB_ASSERT(e != nullptr); - if (e->flags & EntityFlag_Lazy) { - continue; - } - DeclInfo *d = e->decl_info; - GlobalEntityTimingFrame timing_frame = global_entity_timing_begin(e); - check_single_global_entity(c, e, d); - if (e->type != nullptr && is_type_typed(e->type)) { - for (Type *t = nullptr; mpsc_dequeue(&c->soa_types_to_complete, &t); /**/) { - complete_soa_type(c, t, false); - } - - (void)type_size_of(e->type); - (void)type_align_of(e->type); - } - global_entity_timing_end(timing_frame, e); - } - - in_single_threaded_checker_stage.store(false, std::memory_order_relaxed); -} - +#include "checker_global_groups.cpp" gb_internal bool is_string_an_identifier(String s) { isize offset = 0; @@ -7524,7 +7478,9 @@ gb_internal void check_parsed_files(Checker *c) { if (build_context.internal_global_entity_graph) { TIME_SECTION("print global entity graph"); print_global_entity_graph(c); + print_global_groups(&global_groups); } + destroy_global_groups(&global_groups); TIME_SECTION("init preload"); init_preload(c); diff --git a/src/checker.hpp b/src/checker.hpp index 9643fa683..8eecdb6e4 100644 --- a/src/checker.hpp +++ b/src/checker.hpp @@ -917,6 +917,7 @@ gb_internal void check_add_foreign_import_decl(CheckerContext *c, Ast *decl); gb_internal void check_entity_decl(CheckerContext *c, Entity *e, DeclInfo *d, Type *named_type); +gb_internal void global_group_check_edge(CheckerContext *ctx, Entity *e); gb_internal void wait_for_lazy_entity(CheckerContext *c, Entity *e); gb_internal Ast *remove_type_alias_clutter(Ast *node); gb_internal void check_const_decl(CheckerContext *c, Entity *e, Ast *type_expr, Ast *init_expr, Type *named_type); diff --git a/src/checker_global_groups.cpp b/src/checker_global_groups.cpp new file mode 100644 index 000000000..cf3e3924e --- /dev/null +++ b/src/checker_global_groups.cpp @@ -0,0 +1,769 @@ +// Global entities are checked in groups: the strongly connected components of a dependency graph built +// from syntax, in dependency order. A group only names entities of its own or of finished groups, which +// `-internal-check-global-edges` verifies + +struct GlobalGroup { + i32 start; // into `GlobalGroupGraph::members` + i32 count; + bool done; +}; + +struct GlobalGroupGraph { + Array nodes; + PtrMap node_of; + Array offsets; // node -> the nodes it names, as `targets[offsets[v]..offsets[v+1]]` + Array targets; + Array group_of; + Array groups; // every dependency of a group has a lower index + Array members; // nodes, by group, in source order + + bool active; + i32 current_group; + Entity *current_entity; + isize missing_edges; +}; + +gb_global GlobalGroupGraph global_groups; + +struct GlobalGraphWalk { + Scope *scope; + Array *refs; +}; + +gb_internal void global_graph_walk(GlobalGraphWalk *w, Ast *node); + +gb_internal void global_graph_walk_slice(GlobalGraphWalk *w, Slice const &nodes) { + for (Ast *node : nodes) { + global_graph_walk(w, node); + } +} + +gb_internal void global_graph_add_ref(GlobalGraphWalk *w, Entity *e) { + if (e != nullptr) { + array_add(w->refs, e); + } +} + +// NOTE: names bound within the expression (parameters, fields, '$T') are also looked up globally, which at +// worst adds an edge; only procedure bodies are skipped, as they are checked after this stage +gb_internal void global_graph_walk(GlobalGraphWalk *w, Ast *node) { + if (node == nullptr) { + return; + } + switch (node->kind) { + case Ast_Ident: + global_graph_add_ref(w, scope_lookup(w->scope, node->Ident.interned, node->Ident.hash)); + break; + + case Ast_SelectorExpr: { + Ast *expr = node->SelectorExpr.expr; + Ast *selector = node->SelectorExpr.selector; + if (expr != nullptr && expr->kind == Ast_Ident) { + Entity *e = scope_lookup(w->scope, expr->Ident.interned, expr->Ident.hash); + if (e != nullptr && e->kind == Entity_ImportName && selector != nullptr && selector->kind == Ast_Ident) { + global_graph_add_ref(w, scope_lookup_current(e->ImportName.scope, selector->Ident.interned, selector->Ident.hash)); + } else { + global_graph_add_ref(w, e); + } + } else { + global_graph_walk(w, expr); + } + } break; + + case Ast_PolyType: + global_graph_walk(w, node->PolyType.specialization); + break; + case Ast_Ellipsis: + global_graph_walk(w, node->Ellipsis.expr); + break; + case Ast_ProcGroup: + global_graph_walk_slice(w, node->ProcGroup.args); + break; + case Ast_AsmGroup: + global_graph_walk_slice(w, node->AsmGroup.args); + break; + case Ast_ProcLit: + global_graph_walk(w, node->ProcLit.type); + global_graph_walk_slice(w, node->ProcLit.where_clauses); + break; + case Ast_CompoundLit: + global_graph_walk(w, node->CompoundLit.type); + global_graph_walk_slice(w, node->CompoundLit.elems); + global_graph_walk(w, node->CompoundLit.tag); + break; + case Ast_TagExpr: + global_graph_walk(w, node->TagExpr.expr); + break; + case Ast_UnaryExpr: + global_graph_walk(w, node->UnaryExpr.expr); + break; + case Ast_BinaryExpr: + global_graph_walk(w, node->BinaryExpr.left); + global_graph_walk(w, node->BinaryExpr.right); + break; + case Ast_ParenExpr: + global_graph_walk(w, node->ParenExpr.expr); + break; + case Ast_SelectorCallExpr: + global_graph_walk(w, node->SelectorCallExpr.expr); + global_graph_walk(w, node->SelectorCallExpr.call); + break; + case Ast_IndexExpr: + global_graph_walk(w, node->IndexExpr.expr); + global_graph_walk(w, node->IndexExpr.index); + break; + case Ast_MatrixIndexExpr: + global_graph_walk(w, node->MatrixIndexExpr.expr); + global_graph_walk(w, node->MatrixIndexExpr.row_index); + global_graph_walk(w, node->MatrixIndexExpr.column_index); + break; + case Ast_DerefExpr: + global_graph_walk(w, node->DerefExpr.expr); + break; + case Ast_SliceExpr: + global_graph_walk(w, node->SliceExpr.expr); + global_graph_walk(w, node->SliceExpr.low); + global_graph_walk(w, node->SliceExpr.high); + break; + case Ast_CallExpr: + global_graph_walk(w, node->CallExpr.proc); + global_graph_walk_slice(w, node->CallExpr.args); + break; + case Ast_FieldValue: + global_graph_walk(w, node->FieldValue.field); + global_graph_walk(w, node->FieldValue.value); + break; + case Ast_EnumFieldValue: + global_graph_walk(w, node->EnumFieldValue.value); + break; + case Ast_TernaryIfExpr: + global_graph_walk(w, node->TernaryIfExpr.x); + global_graph_walk(w, node->TernaryIfExpr.cond); + global_graph_walk(w, node->TernaryIfExpr.y); + break; + case Ast_TernaryWhenExpr: + global_graph_walk(w, node->TernaryWhenExpr.x); + global_graph_walk(w, node->TernaryWhenExpr.cond); + global_graph_walk(w, node->TernaryWhenExpr.y); + break; + case Ast_OrElseExpr: + global_graph_walk(w, node->OrElseExpr.x); + global_graph_walk(w, node->OrElseExpr.y); + break; + case Ast_OrReturnExpr: + global_graph_walk(w, node->OrReturnExpr.expr); + break; + case Ast_OrBranchExpr: + global_graph_walk(w, node->OrBranchExpr.expr); + break; + case Ast_TypeAssertion: + global_graph_walk(w, node->TypeAssertion.expr); + global_graph_walk(w, node->TypeAssertion.type); + break; + case Ast_TypeCast: + global_graph_walk(w, node->TypeCast.type); + global_graph_walk(w, node->TypeCast.expr); + break; + case Ast_AutoCast: + global_graph_walk(w, node->AutoCast.expr); + break; + + case Ast_Field: + global_graph_walk(w, node->Field.type); + global_graph_walk(w, node->Field.default_value); + break; + case Ast_BitFieldField: + global_graph_walk(w, node->BitFieldField.type); + global_graph_walk(w, node->BitFieldField.bit_size); + break; + case Ast_FieldList: + global_graph_walk_slice(w, node->FieldList.list); + break; + + case Ast_TypeidType: + global_graph_walk(w, node->TypeidType.specialization); + break; + case Ast_HelperType: + global_graph_walk(w, node->HelperType.type); + break; + case Ast_DistinctType: + global_graph_walk(w, node->DistinctType.type); + break; + case Ast_ProcType: + global_graph_walk(w, node->ProcType.params); + global_graph_walk(w, node->ProcType.results); + break; + case Ast_RelativeType: + global_graph_walk(w, node->RelativeType.tag); + global_graph_walk(w, node->RelativeType.type); + break; + case Ast_PointerType: + global_graph_walk(w, node->PointerType.type); + global_graph_walk(w, node->PointerType.tag); + break; + case Ast_MultiPointerType: + global_graph_walk(w, node->MultiPointerType.type); + break; + case Ast_ArrayType: + global_graph_walk(w, node->ArrayType.count); + global_graph_walk(w, node->ArrayType.elem); + global_graph_walk(w, node->ArrayType.tag); + break; + case Ast_DynamicArrayType: + global_graph_walk(w, node->DynamicArrayType.elem); + global_graph_walk(w, node->DynamicArrayType.tag); + break; + case Ast_FixedCapacityDynamicArrayType: + global_graph_walk(w, node->FixedCapacityDynamicArrayType.elem); + global_graph_walk(w, node->FixedCapacityDynamicArrayType.capacity); + global_graph_walk(w, node->FixedCapacityDynamicArrayType.tag); + break; + case Ast_StructType: + global_graph_walk_slice(w, node->StructType.fields); + global_graph_walk(w, node->StructType.polymorphic_params); + global_graph_walk(w, node->StructType.align); + global_graph_walk(w, node->StructType.min_field_align); + global_graph_walk(w, node->StructType.max_field_align); + global_graph_walk_slice(w, node->StructType.where_clauses); + break; + case Ast_UnionType: + global_graph_walk_slice(w, node->UnionType.variants); + global_graph_walk(w, node->UnionType.polymorphic_params); + global_graph_walk_slice(w, node->UnionType.where_clauses); + break; + case Ast_EnumType: + global_graph_walk(w, node->EnumType.base_type); + for (Ast *field : node->EnumType.fields) { + if (field->kind == Ast_EnumFieldValue) { + global_graph_walk(w, field->EnumFieldValue.value); + } + } + break; + case Ast_BitSetType: + global_graph_walk(w, node->BitSetType.elem); + global_graph_walk(w, node->BitSetType.underlying); + break; + case Ast_BitFieldType: + global_graph_walk(w, node->BitFieldType.backing_type); + global_graph_walk_slice(w, node->BitFieldType.fields); + break; + case Ast_MapType: + global_graph_walk(w, node->MapType.count); + global_graph_walk(w, node->MapType.key); + global_graph_walk(w, node->MapType.value); + break; + case Ast_MatrixType: + global_graph_walk(w, node->MatrixType.row_count); + global_graph_walk(w, node->MatrixType.column_count); + global_graph_walk(w, node->MatrixType.elem); + break; + + case Ast_AsmTemplate: + global_graph_walk(w, node->AsmTemplate.signature); + global_graph_walk_slice(w, node->AsmTemplate.specs); + global_graph_walk_slice(w, node->AsmTemplate.clobbers); + global_graph_walk_slice(w, node->AsmTemplate.instructions); + break; + case Ast_AsmSpec: + global_graph_walk(w, node->AsmSpec.type); + global_graph_walk(w, node->AsmSpec.value); + for (Ast *d : node->AsmSpec.directives) { + global_graph_walk(w, d); + } + break; + case Ast_AsmClobber: + global_graph_walk(w, node->AsmClobber.value); + break; + case Ast_AsmInstruction: + global_graph_walk_slice(w, node->AsmInstruction.operands); + break; + case Ast_AsmMemoryTerm: + global_graph_walk(w, node->AsmMemoryTerm.operand); + global_graph_walk(w, node->AsmMemoryTerm.scale); + break; + case Ast_AsmMemoryOperand: + global_graph_walk(w, node->AsmMemoryOperand.segment_override); + global_graph_walk_slice(w, node->AsmMemoryOperand.terms); + global_graph_walk(w, node->AsmMemoryOperand.type); + break; + case Ast_AsmRegisterGroup: + for (Ast *r : node->AsmRegisterGroup.registers) { + global_graph_walk(w, r); + } + global_graph_walk(w, node->AsmRegisterGroup.type); + break; + case Ast_AsmDirective: + global_graph_walk_slice(w, node->AsmDirective.operands); + break; + } +} + +gb_internal void global_graph_walk_entity(GlobalGraphWalk *w, Entity *e, DeclInfo *d) { + w->scope = d->scope; + global_graph_walk(w, d->type_expr); + global_graph_walk(w, d->init_expr); + for (Ast *attr : d->attributes) { + if (attr->kind != Ast_Attribute) { + continue; + } + for (Ast *elem : attr->Attribute.elems) { + if (elem->kind == Ast_FieldValue) { + global_graph_walk(w, elem->FieldValue.value); + } + } + } + if (e->kind == Entity_Procedure) { + global_graph_walk(w, e->Procedure.foreign_library_ident); + } else if (e->kind == Entity_Variable) { + global_graph_walk(w, e->Variable.foreign_library_ident); + } +} + +gb_internal bool is_global_graph_node(Entity *e) { + if (e->state == EntityState_Resolved) { + return false; + } + DeclInfo *d = e->decl_info; + if (d == nullptr || e->scope == nullptr || d->scope != e->scope || (e->scope->flags & ScopeFlag_File) == 0) { + return false; + } + switch (e->kind) { + case Entity_Constant: + case Entity_TypeName: + case Entity_Variable: + case Entity_Procedure: + case Entity_ProcGroup: + case Entity_AsmTemplate: + return true; + } + return false; +} + +gb_internal i32 global_graph_add_node(GlobalGroupGraph *g, Entity *e) { + i32 *found = map_get(&g->node_of, e); + if (found != nullptr) { + return *found; + } + i32 v = cast(i32)g->nodes.count; + map_set(&g->node_of, e, v); + array_add(&g->nodes, e); + return v; +} + +gb_internal u64 global_group_random(u64 *state) { + *state = *state*6364136223846793005ull + 1442695040888963407ull; + return *state >> 33; +} + +// The nodes `[lo, hi)` walked on one thread; a name that is not a node yet is kept as an entity in `refs` +struct GlobalGraphWalkChunk { + GlobalGroupGraph *g; + i32 lo, hi; + Array targets; + Array target_ends; // per node + Array refs; + Array ref_ends; // per node +}; + +gb_internal WORKER_TASK_PROC(global_graph_walk_worker) { + GlobalGraphWalkChunk *chunk = cast(GlobalGraphWalkChunk *)data; + GlobalGroupGraph *g = chunk->g; + + auto refs = array_make(heap_allocator(), 0, 64); + defer (array_free(&refs)); + GlobalGraphWalk w = {}; + w.refs = &refs; + for (i32 v = chunk->lo; v < chunk->hi; v++) { + Entity *e = g->nodes[v]; + array_clear(&refs); + global_graph_walk_entity(&w, e, e->decl_info); + for (Entity *r : refs) { + i32 *found = map_get(&g->node_of, r); + if (found != nullptr) { + array_add(&chunk->targets, *found); + } else if (r->flags & EntityFlag_Lazy) { + array_add(&chunk->refs, r); + } + } + array_add(&chunk->target_ends, cast(i32)chunk->targets.count); + array_add(&chunk->ref_ends, cast(i32)chunk->refs.count); + } + return 0; +} + +gb_internal void build_global_groups(Checker *c, GlobalGroupGraph *g) { + array_init(&g->nodes, heap_allocator(), 0, c->info.entities.count); + map_init(&g->node_of, c->info.entities.count); + for (Entity *e : c->info.entities) { + if ((e->flags & EntityFlag_Lazy) == 0 && is_global_graph_node(e)) { + global_graph_add_node(g, e); + } + } + + // NOTE: walked in parallel, as nothing writes to the scopes now + i32 const CHUNK_SIZE = 64; + i32 initial_count = cast(i32)g->nodes.count; + auto chunks = array_make(heap_allocator(), (initial_count + CHUNK_SIZE-1)/CHUNK_SIZE); + defer (array_free(&chunks)); + for (isize i = 0; i < chunks.count; i++) { + GlobalGraphWalkChunk *chunk = &chunks[i]; + *chunk = {}; + chunk->g = g; + chunk->lo = cast(i32)(i*CHUNK_SIZE); + chunk->hi = gb_min(chunk->lo + CHUNK_SIZE, initial_count); + array_init(&chunk->targets, heap_allocator(), 0, 4*CHUNK_SIZE); + array_init(&chunk->target_ends, heap_allocator(), 0, CHUNK_SIZE); + array_init(&chunk->refs, heap_allocator(), 0); + array_init(&chunk->ref_ends, heap_allocator(), 0, CHUNK_SIZE); + thread_pool_add_task(global_graph_walk_worker, chunk); + } + thread_pool_wait(); + + auto edge_from = array_make(heap_allocator(), 0, 4*g->nodes.count); + auto edge_to = array_make(heap_allocator(), 0, 4*g->nodes.count); + auto refs = array_make(heap_allocator(), 0, 64); + defer (array_free(&edge_from)); + defer (array_free(&edge_to)); + defer (array_free(&refs)); + + auto add_ref = [&](i32 v, Entity *r) { + i32 *found = map_get(&g->node_of, r); + if (found != nullptr) { + array_add(&edge_from, v); + array_add(&edge_to, *found); + } else if ((r->flags & EntityFlag_Lazy) && is_global_graph_node(r)) { + // NOTE: a lazy entity becomes a node once a node names it + array_add(&edge_from, v); + array_add(&edge_to, global_graph_add_node(g, r)); + } + }; + + GlobalGraphWalk w = {}; + w.refs = &refs; + i32 first_of_decl = -1; + for (i32 v = 0; v < g->nodes.count; v++) { + Entity *e = g->nodes[v]; + DeclInfo *d = e->decl_info; + + // NOTE: entities sharing one declaration share its AST, e.g. `a, b: struct{x: int}`, so they share a group; + // in source order they are adjacent, and lazy ones are only checked under `lazy_mutex` + if (first_of_decl >= 0 && d->decl_node != nullptr && g->nodes[first_of_decl]->decl_info->decl_node == d->decl_node) { + array_add(&edge_from, v); + array_add(&edge_to, first_of_decl); + array_add(&edge_from, first_of_decl); + array_add(&edge_to, v); + } else { + first_of_decl = v; + } + + if (v < initial_count) { + GlobalGraphWalkChunk *chunk = &chunks[v / CHUNK_SIZE]; + i32 k = v - chunk->lo; + for (i32 i = k > 0 ? chunk->target_ends[k-1] : 0; i < chunk->target_ends[k]; i++) { + array_add(&edge_from, v); + array_add(&edge_to, chunk->targets[i]); + } + for (i32 i = k > 0 ? chunk->ref_ends[k-1] : 0; i < chunk->ref_ends[k]; i++) { + add_ref(v, chunk->refs[i]); + } + } else { + array_clear(&refs); + global_graph_walk_entity(&w, e, d); + for (Entity *r : refs) { + add_ref(v, r); + } + } + } + for (GlobalGraphWalkChunk &chunk : chunks) { + array_free(&chunk.targets); + array_free(&chunk.target_ends); + array_free(&chunk.refs); + array_free(&chunk.ref_ends); + } + + i32 node_count = cast(i32)g->nodes.count; + array_init(&g->offsets, heap_allocator(), node_count+1); + array_init(&g->targets, heap_allocator(), edge_to.count); + for (i32 v = 0; v <= node_count; v++) { + g->offsets[v] = 0; + } + for (i32 from : edge_from) { + g->offsets[from+1] += 1; + } + for (i32 v = 0; v < node_count; v++) { + g->offsets[v+1] += g->offsets[v]; + } + { + auto fill = array_clone(heap_allocator(), g->offsets); + defer (array_free(&fill)); + for (isize i = 0; i < edge_from.count; i++) { + g->targets[fill[edge_from[i]]++] = edge_to[i]; + } + } + + array_init(&g->group_of, heap_allocator(), node_count); + i32 group_count = global_graph_scc(node_count, g->offsets, g->targets, &g->group_of); + + array_init(&g->groups, heap_allocator(), group_count); + array_init(&g->members, heap_allocator(), node_count); + for (i32 gi = 0; gi < group_count; gi++) { + g->groups[gi] = {}; + } + for (i32 v = 0; v < node_count; v++) { + g->groups[g->group_of[v]].count += 1; + } + i32 start = 0; + for (i32 gi = 0; gi < group_count; gi++) { + g->groups[gi].start = start; + start += g->groups[gi].count; + g->groups[gi].count = 0; + } + // NOTE: in source order, as `c->info.entities` is sorted; lazy nodes come last, but are not checked here + for (i32 v = 0; v < node_count; v++) { + GlobalGroup *group = &g->groups[g->group_of[v]]; + g->members[group->start + group->count++] = v; + } +} + +gb_internal void global_group_print_entity(Entity *e) { + if (e == nullptr) { + gb_printf_err("?"); + return; + } + global_graph_print_entity(e); +} + +// Called when `e` starts being checked: it must be in the current group or a finished one +gb_internal void global_group_check_edge(CheckerContext *ctx, Entity *e) { + GlobalGroupGraph *g = &global_groups; + if (!g->active) { + return; + } + i32 *v = map_get(&g->node_of, e); + if (v == nullptr) { + if (!is_global_graph_node(e)) { + return; + } + } else { + i32 gi = g->group_of[*v]; + if (gi == g->current_group || g->groups[gi].done) { + return; + } + } + + g->missing_edges += 1; + if (build_context.internal_check_global_edges) { + Entity *by = ctx->decl ? ctx->decl->entity.load() : nullptr; + gb_printf_err("Missing global dependency: "); + global_group_print_entity(by); + gb_printf_err(" needs "); + global_group_print_entity(e); + gb_printf_err(v == nullptr ? ", which is not in the graph" : ""); + if (g->current_entity != by) { + gb_printf_err(", while checking "); + global_group_print_entity(g->current_entity); + } + gb_printf_err("\n"); + } +} + +// NOTE: members in a fixed order, as which member of a cycle is entered first can decide whether it checks, +// e.g. an enum whose values are `union_variant_index`es of a union with pointers back to it +gb_internal void check_global_group(Checker *c, GlobalGroupGraph *g, i32 gi) { + GlobalGroup *group = &g->groups[gi]; + i32 *members = g->members.data + group->start; + + g->current_group = gi; + for (i32 k = 0; k < group->count; k++) { + Entity *e = g->nodes[members[k]]; + if (e->flags & EntityFlag_Lazy) { + // NOTE: only checked when something uses it; the group orders it after what it names + continue; + } + g->current_entity = e; + GlobalEntityTimingFrame timing_frame = global_entity_timing_begin(e); + check_single_global_entity(c, e, e->decl_info); + if (e->type != nullptr && is_type_typed(e->type)) { + for (Type *t = nullptr; mpsc_dequeue(&c->soa_types_to_complete, &t); /**/) { + complete_soa_type(c, t, false); + } + + (void)type_size_of(e->type); + (void)type_align_of(e->type); + } + global_entity_timing_end(timing_frame, e); + } + group->done = true; + g->current_group = -1; + g->current_entity = nullptr; +} + +// Groups in dependency order; with `-internal-shuffle-global-entities`, a random one of the groups whose +// dependencies are done, as a parallel checker might +gb_internal void check_global_groups(Checker *c, GlobalGroupGraph *g) { + i32 group_count = cast(i32)g->groups.count; + u64 seed = build_context.internal_shuffle_global_entities; + if (seed == 0) { + for (i32 gi = 0; gi < group_count; gi++) { + check_global_group(c, g, gi); + } + return; + } + + auto dependents = array_make >(heap_allocator(), group_count); + auto dep_count = array_make (heap_allocator(), group_count); + auto seen = array_make (heap_allocator(), group_count); + auto ready = array_make (heap_allocator(), 0, group_count); + defer ({ + for (auto &d : dependents) { + array_free(&d); + } + array_free(&dependents); + }); + defer (array_free(&dep_count)); + defer (array_free(&seen)); + defer (array_free(&ready)); + + for (i32 gi = 0; gi < group_count; gi++) { + dep_count[gi] = 0; + dependents[gi] = {}; + seen[gi] = -1; + } + for (i32 gi = 0; gi < group_count; gi++) { + GlobalGroup const &group = g->groups[gi]; + for (i32 k = 0; k < group.count; k++) { + i32 v = g->members[group.start + k]; + for (i32 i = g->offsets[v]; i < g->offsets[v+1]; i++) { + i32 dep = g->group_of[g->targets[i]]; + if (dep != gi && seen[dep] != gi) { + seen[dep] = gi; + dep_count[gi] += 1; + if (dependents[dep].allocator.proc == nullptr) { + array_init(&dependents[dep], heap_allocator()); + } + array_add(&dependents[dep], gi); + } + } + } + if (dep_count[gi] == 0) { + array_add(&ready, gi); + } + } + + u64 state = seed; + isize checked = 0; + while (ready.count > 0) { + isize i = cast(isize)(global_group_random(&state) % cast(u64)ready.count); + i32 gi = ready[i]; + ready[i] = ready[ready.count-1]; + array_pop(&ready); + + check_global_group(c, g, gi); + checked += 1; + for (i32 next : dependents[gi]) { + if (--dep_count[next] == 0) { + array_add(&ready, next); + } + } + } + GB_ASSERT(checked == group_count); +} + +gb_internal void destroy_global_groups(GlobalGroupGraph *g) { + array_free(&g->nodes); + map_destroy(&g->node_of); + array_free(&g->offsets); + array_free(&g->targets); + array_free(&g->group_of); + array_free(&g->groups); + array_free(&g->members); +} + +// -internal-global-entity-graph: the graph the groups come from, weighted by the measured self times +gb_internal void print_global_groups(GlobalGroupGraph *g) { + u64 const freq = time_stamp__freq(); + i32 group_count = cast(i32)g->groups.count; + + auto ticks = array_make(heap_allocator(), group_count); + auto path = array_make(heap_allocator(), group_count); + auto seen = array_make(heap_allocator(), group_count); + defer (array_free(&ticks)); + defer (array_free(&path)); + defer (array_free(&seen)); + + u64 total = 0; + i32 largest = 0; + isize cyclic = 0; + mutex_lock(&global_entity_time_mutex); + for (i32 gi = 0; gi < group_count; gi++) { + GlobalGroup const &group = g->groups[gi]; + ticks[gi] = 0; + seen[gi] = -1; + for (i32 k = 0; k < group.count; k++) { + GlobalEntityTime *t = map_get(&global_entity_times, g->nodes[g->members[group.start + k]]); + ticks[gi] += t ? t->ticks : 0; + } + total += ticks[gi]; + largest = gb_max(largest, group.count); + cyclic += group.count > 1; + } + mutex_unlock(&global_entity_time_mutex); + + u64 critical = 0; + for (i32 gi = 0; gi < group_count; gi++) { + GlobalGroup const &group = g->groups[gi]; + u64 longest = 0; + for (i32 k = 0; k < group.count; k++) { + i32 v = g->members[group.start + k]; + for (i32 i = g->offsets[v]; i < g->offsets[v+1]; i++) { + i32 dep = g->group_of[g->targets[i]]; + if (dep != gi && seen[dep] != gi) { + seen[dep] = gi; + longest = gb_max(longest, path[dep]); + } + } + } + path[gi] = ticks[gi] + longest; + critical = gb_max(critical, path[gi]); + } + + f64 total_ms = global_graph_ms(total, freq); + f64 critical_ms = global_graph_ms(critical, freq); + gb_printf_err("Global groups (syntactic graph, as scheduled)\n"); + gb_printf_err(" nodes: %td, edges: %td, groups: %d (%td with a cycle), largest has %d entities\n", + g->nodes.count, g->targets.count, group_count, cyclic, largest); + gb_printf_err(" critical path: %.3f ms of %.3f ms -> at most %.2fx speedup\n", + critical_ms, total_ms, critical_ms > 0 ? total_ms/critical_ms : 0.0); + gb_printf_err(" missing edges: %td\n", g->missing_edges); +} + +gb_internal void check_all_global_entities(Checker *c) { + in_single_threaded_checker_stage.store(true, std::memory_order_relaxed); + + // NOTE(bill): the runtime types the checker looks up by name rather than through a declaration + init_preload(c); + { + u32 hash = 0; + InternedString name = string_interner_insert(str_lit("Load_Directory_File"), 0, &hash); + if (scope_lookup_current(c->info.runtime_package->scope, name, hash) != nullptr) { + init_core_load_directory_file(c); + } + } + + TIME_SECTION("check all global entities - build groups"); + GlobalGroupGraph *g = &global_groups; + build_global_groups(c, g); + + TIME_SECTION("check all global entities - check groups"); + g->active = true; + g->current_group = -1; + check_global_groups(c, g); + g->active = false; + + if (build_context.internal_check_global_edges && g->missing_edges > 0) { + gb_printf_err("%td missing global dependencies\n", g->missing_edges); + gb_exit(1); + } + + in_single_threaded_checker_stage.store(false, std::memory_order_relaxed); +} diff --git a/src/main.cpp b/src/main.cpp index 5a3f4e3da..9d661bb16 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -535,6 +535,7 @@ enum BuildFlagKind { BuildFlag_InternalEnableRVO, BuildFlag_InternalGlobalEntityGraph, BuildFlag_InternalShuffleGlobalEntities, + BuildFlag_InternalCheckGlobalEdges, BuildFlag_Sanitize, BuildFlag_LTO, @@ -799,6 +800,7 @@ gb_internal bool parse_build_flags(Array args) { add_flag(&build_flags, BuildFlag_InternalEnableRVO, str_lit("internal-enable-rvo"), BuildFlagParam_None, Command_all); add_flag(&build_flags, BuildFlag_InternalGlobalEntityGraph, str_lit("internal-global-entity-graph"), BuildFlagParam_None, Command__does_check); add_flag(&build_flags, BuildFlag_InternalShuffleGlobalEntities, str_lit("internal-shuffle-global-entities"), BuildFlagParam_Integer, Command__does_check); + add_flag(&build_flags, BuildFlag_InternalCheckGlobalEdges, str_lit("internal-check-global-edges"), BuildFlagParam_None, Command__does_check); add_flag(&build_flags, BuildFlag_Sanitize, str_lit("sanitize"), BuildFlagParam_String, Command__does_build, true); @@ -1855,6 +1857,9 @@ gb_internal bool parse_build_flags(Array args) { GB_ASSERT(value.kind == ExactValue_Integer); build_context.internal_shuffle_global_entities = cast(u64)big_int_to_i64(&value.value_integer); break; + case BuildFlag_InternalCheckGlobalEdges: + build_context.internal_check_global_edges = true; + break; case BuildFlag_Sanitize: From f7ee910f936f88d446d5454fa0075d69892dc2fb Mon Sep 17 00:00:00 2001 From: gingerBill Date: Thu, 1 Oct 2026 17:40:27 +0100 Subject: [PATCH 06/17] Merge `checker_global_*.cpp` into `checker_global.cpp`; base `-internal-global-entity-graph` on the scheduled groups --- src/checker.cpp | 5 +- src/checker.hpp | 9 + src/checker_global.cpp | 1570 +++++++++++++++++++++++++++++++++ src/checker_global_graph.cpp | 539 ----------- src/checker_global_groups.cpp | 769 ---------------- src/checker_global_when.cpp | 474 ---------- 6 files changed, 1580 insertions(+), 1786 deletions(-) create mode 100644 src/checker_global.cpp delete mode 100644 src/checker_global_graph.cpp delete mode 100644 src/checker_global_groups.cpp delete mode 100644 src/checker_global_when.cpp diff --git a/src/checker.cpp b/src/checker.cpp index fa454f404..7acd8f3f8 100644 --- a/src/checker.cpp +++ b/src/checker.cpp @@ -4574,7 +4574,6 @@ gb_internal DECL_ATTRIBUTE_PROC(asm_decl_attribute) { return false; } -#include "checker_global_graph.cpp" #include "check_expr.cpp" #include "check_builtin.cpp" #include "check_type.cpp" @@ -5339,7 +5338,6 @@ gb_internal void check_single_global_entity(Checker *c, Entity *e, DeclInfo *d) check_entity_decl(ctx, e, d, nullptr); } -#include "checker_global_groups.cpp" gb_internal bool is_string_an_identifier(String s) { isize offset = 0; @@ -5996,7 +5994,7 @@ gb_internal void check_export_entities(Checker *c) { thread_pool_wait(); } -#include "checker_global_when.cpp" +#include "checker_global.cpp" gb_internal void check_import_entities(Checker *c) { TEMPORARY_ALLOCATOR_GUARD(); @@ -7477,7 +7475,6 @@ gb_internal void check_parsed_files(Checker *c) { if (build_context.internal_global_entity_graph) { TIME_SECTION("print global entity graph"); - print_global_entity_graph(c); print_global_groups(&global_groups); } destroy_global_groups(&global_groups); diff --git a/src/checker.hpp b/src/checker.hpp index 8eecdb6e4..05eed75ef 100644 --- a/src/checker.hpp +++ b/src/checker.hpp @@ -918,6 +918,15 @@ gb_internal void check_add_foreign_import_decl(CheckerContext *c, Ast *decl); gb_internal void check_entity_decl(CheckerContext *c, Entity *e, DeclInfo *d, Type *named_type); gb_internal void global_group_check_edge(CheckerContext *ctx, Entity *e); + +// -internal-global-entity-graph +struct GlobalEntityTimingFrame { + u64 start; + u64 saved_child_ticks; + bool active; +}; +gb_internal GlobalEntityTimingFrame global_entity_timing_begin(Entity *e); +gb_internal void global_entity_timing_end(GlobalEntityTimingFrame const &f, Entity *e); gb_internal void wait_for_lazy_entity(CheckerContext *c, Entity *e); gb_internal Ast *remove_type_alias_clutter(Ast *node); gb_internal void check_const_decl(CheckerContext *c, Entity *e, Ast *type_expr, Ast *init_expr, Type *named_type); diff --git a/src/checker_global.cpp b/src/checker_global.cpp new file mode 100644 index 000000000..cf9bb6e84 --- /dev/null +++ b/src/checker_global.cpp @@ -0,0 +1,1570 @@ +// Global declarations: 'when's and 'foreign' blocks resolved on demand in `check_import_entities`, then the +// global entities checked in groups of a dependency graph, plus what `-internal-global-entity-graph` reports + +// Timing for -internal-global-entity-graph: the self time of each global entity, and the parts of +// `check_import_entities` + +struct GlobalEntityTime { + u64 ticks; + bool in_global_loop; +}; + +enum GlobalImportStagePart { + GlobalImportStage_Imports, + GlobalImportStage_Placeholders, + GlobalImportStage_DeclSources, + GlobalImportStage_TypeAliases, + GlobalImportStage_DelayedExprs, + + GlobalImportStage_COUNT, +}; + +gb_global char const *global_import_stage_names[GlobalImportStage_COUNT] = { + "imports", + "'when' and 'foreign' placeholders", + "resolve 'when' and 'foreign' blocks", + "type alias correction", + "delayed expressions (#assert etc.)", +}; + +gb_global u64 global_import_stage_ticks[GlobalImportStage_COUNT]; + +gb_internal u64 global_import_stage_begin(void) { + return build_context.internal_global_entity_graph ? time_stamp_time_now() : 0; +} + +gb_internal void global_import_stage_end(GlobalImportStagePart part, u64 start) { + if (build_context.internal_global_entity_graph) { + global_import_stage_ticks[part] += time_stamp_time_now() - start; + } +} + +gb_global BlockingMutex global_entity_time_mutex; +gb_global PtrMap global_entity_times; +gb_thread_local u64 global_entity_child_ticks; + +gb_internal GlobalEntityTimingFrame global_entity_timing_begin(Entity *e) { + GlobalEntityTimingFrame f = {}; + if (!build_context.internal_global_entity_graph) { + return f; + } + if (e->scope == nullptr || (e->scope->flags & ScopeFlag_File) == 0) { + return f; + } + + f.saved_child_ticks = global_entity_child_ticks; + global_entity_child_ticks = 0; + + f.active = true; + f.start = time_stamp_time_now(); + return f; +} + +gb_internal void global_entity_timing_end(GlobalEntityTimingFrame const &f, Entity *e) { + if (!f.active) { + return; + } + u64 total = time_stamp_time_now() - f.start; + u64 self = total - gb_min(total, global_entity_child_ticks); + global_entity_child_ticks = f.saved_child_ticks + total; + + bool in_global_loop = in_single_threaded_checker_stage.load(std::memory_order_relaxed); + + MUTEX_GUARD(&global_entity_time_mutex); + GlobalEntityTime *found = map_get(&global_entity_times, e); + if (found) { + found->ticks += self; + } else { + map_set(&global_entity_times, e, GlobalEntityTime{self, in_global_loop}); + } +} + + +// Global 'when's and 'foreign' blocks: every name one may declare is a placeholder in its scope, and the +// first lookup of a placeholder resolves them, so the order of files and declarations does not matter + +struct GlobalDeclSource { + Ast * node; // WhenStmt or ForeignBlockDecl + AstFile * file; + GlobalDeclSource *parent; + bool in_else; // within the else branch of `parent` + bool reachable; + bool reported_cycle; + EntityState state; + ForeignContext foreign_context; // of a resolved 'foreign' block +}; + +struct GlobalDeclSourceFrame { + GlobalDeclSource *source; + InternedString needs; // the placeholder being resolved for it +}; + +gb_global Array global_decl_sources; +gb_global Array global_decl_source_stack; +gb_global Array global_placeholder_scopes; +gb_global CheckerContext global_decl_source_export_ctx; +gb_global UntypedExprInfoMap global_decl_source_export_untyped; + +enum : u8 { + PlaceholderScope_File = 1<<0, + PlaceholderScope_Pkg = 1<<1, +}; + +// -1 when 'private' has a value that is not a string literal +gb_internal i32 syntactic_visibility(Array const &attributes) { + for (Ast *attr : attributes) { + if (attr->kind != Ast_Attribute) { + continue; + } + for (Ast *elem : attr->Attribute.elems) { + if (elem->kind == Ast_Ident && elem->Ident.token.string == "private") { + return EntityVisiblity_PrivateToPackage; + } + if (elem->kind == Ast_FieldValue && + elem->FieldValue.field->kind == Ast_Ident && + elem->FieldValue.field->Ident.token.string == "private") { + Ast *value = elem->FieldValue.value; + if (value != nullptr && value->tav.value.kind == ExactValue_String) { + return value->tav.value.value_string == "file" ? EntityVisiblity_PrivateToFile : EntityVisiblity_PrivateToPackage; + } + return -1; + } + } + } + return EntityVisiblity_Public; +} + +gb_internal bool has_syntactic_attribute(Array const &attributes, String const &name) { + for (Ast *attr : attributes) { + if (attr->kind != Ast_Attribute) { + continue; + } + for (Ast *elem : attr->Attribute.elems) { + Ast *field = elem->kind == Ast_FieldValue ? elem->FieldValue.field : elem; + if (field->kind == Ast_Ident && field->Ident.token.string == name) { + return true; + } + } + } + return false; +} + +gb_internal void add_placeholder(Scope *s, InternedString name, GlobalDeclSource *src) { + if (name.value == 0 || name.is_blank()) { + return; + } + if (s->placeholders == nullptr) { + s->placeholders = permanent_alloc_item>(); + map_init(s->placeholders); + array_add(&global_placeholder_scopes, s); + } + u64 key = name.value; + for (auto *e = multi_map_find_first(s->placeholders, key); e != nullptr; e = multi_map_find_next(s->placeholders, e)) { + if (e->value == src) { + return; + } + } + multi_map_insert(s->placeholders, key, src); +} + +gb_internal void add_placeholders(AstFile *f, u8 scopes, InternedString name, GlobalDeclSource *src) { + if (scopes & PlaceholderScope_File) { + add_placeholder(f->scope, name, src); + } + if (scopes & PlaceholderScope_Pkg) { + add_placeholder(f->pkg->scope, name, src); + } +} + +gb_internal GlobalDeclSource *add_global_decl_source(Ast *node, AstFile *f, GlobalDeclSource *parent, bool in_else) { + GlobalDeclSource *src = permanent_alloc_item(); + src->node = node; + src->file = f; + src->parent = parent; + src->in_else = in_else; + src->reachable = true; + src->state = EntityState_Unresolved; + array_add(&global_decl_sources, src); + return src; +} + +gb_internal void scan_global_decl_sources(AstFile *f, Slice const &stmts, GlobalDeclSource *owner, bool in_else, i32 foreign_visibility); + +gb_internal void scan_global_when_stmt(AstFile *f, Ast *node, GlobalDeclSource *parent, bool in_else, i32 foreign_visibility) { + ast_node(ws, WhenStmt, node); + GlobalDeclSource *src = add_global_decl_source(node, f, parent, in_else); + if (ws->body != nullptr && ws->body->kind == Ast_BlockStmt) { + scan_global_decl_sources(f, ws->body->BlockStmt.stmts, src, false, foreign_visibility); + } + if (ws->else_stmt != nullptr) { + switch (ws->else_stmt->kind) { + case Ast_BlockStmt: + scan_global_decl_sources(f, ws->else_stmt->BlockStmt.stmts, src, true, foreign_visibility); + break; + case Ast_WhenStmt: + scan_global_when_stmt(f, ws->else_stmt, src, true, foreign_visibility); + break; + } + } +} + +gb_internal void scan_global_decl_sources(AstFile *f, Slice const &stmts, GlobalDeclSource *owner, bool in_else, i32 foreign_visibility) { + // NOTE(bill): `owner == nullptr` is the file scope itself, whose other declarations are already collected + for (Ast *decl : stmts) { + switch (decl->kind) { + case_ast_node(vd, ValueDecl, decl); + if (owner == nullptr) { + break; + } + i32 visibility = syntactic_visibility(vd->attributes); + if (visibility == EntityVisiblity_Public) { + visibility = foreign_visibility; + } + if (visibility == EntityVisiblity_Public && (f->flags & AstFile_IsPrivateFile)) { + visibility = EntityVisiblity_PrivateToFile; + } + u8 scopes = PlaceholderScope_Pkg; + if (visibility == EntityVisiblity_PrivateToFile) { + scopes = PlaceholderScope_File; + } else if (visibility < 0) { + scopes = PlaceholderScope_File|PlaceholderScope_Pkg; + } + for (Ast *name : vd->names) { + if (name->kind == Ast_Ident) { + add_placeholders(f, scopes, name->Ident.interned, owner); + } + } + case_end; + + case_ast_node(fl, ForeignImportDecl, decl); + if (owner == nullptr) { + break; + } + String library_name = fl->library_name.string; + if (library_name.len == 0 && fl->fullpaths.count != 0) { + library_name = path_to_entity_name(fl->library_name.string, fl->fullpaths[0]); + } + if (library_name.len != 0) { + u8 scopes = has_syntactic_attribute(fl->attributes, str_lit("export")) ? PlaceholderScope_Pkg : PlaceholderScope_File; + add_placeholders(f, scopes, string_interner_insert(library_name), owner); + } + case_end; + + case_ast_node(fb, ForeignBlockDecl, decl); + GlobalDeclSource *src = add_global_decl_source(decl, f, owner, in_else); + if (fb->body != nullptr && fb->body->kind == Ast_BlockStmt) { + scan_global_decl_sources(f, fb->body->BlockStmt.stmts, src, false, syntactic_visibility(fb->attributes)); + } + case_end; + + case_ast_node(ws, WhenStmt, decl); + scan_global_when_stmt(f, decl, owner, in_else, foreign_visibility); + case_end; + + case_ast_node(es, ExprStmt, decl); + if (owner == nullptr && es->expr->kind == Ast_CallExpr && + es->expr->CallExpr.proc->kind == Ast_BasicDirective && + (decl->state_flags & StateFlag_BeenHandled) == 0) { + decl->state_flags |= StateFlag_BeenHandled; + array_add(&f->delayed_decls_queues[AstDelayQueue_Expr], es->expr); + } + case_end; + } + } +} + +gb_internal bool is_global_decl_source_in_when(GlobalDeclSource *src) { + for (; src != nullptr; src = src->parent) { + if (src->node->kind == Ast_WhenStmt) { + return true; + } + } + return false; +} + +gb_internal Slice global_decl_source_taken_stmts(GlobalDeclSource *src) { + if (src->node->kind == Ast_ForeignBlockDecl) { + Ast *body = src->node->ForeignBlockDecl.body; + if (body != nullptr && body->kind == Ast_BlockStmt) { + return body->BlockStmt.stmts; + } + return {}; + } + ast_node(ws, WhenStmt, src->node); + if (ws->determined_cond) { + if (ws->body != nullptr && ws->body->kind == Ast_BlockStmt) { + return ws->body->BlockStmt.stmts; + } + } else if (ws->else_stmt != nullptr && ws->else_stmt->kind == Ast_BlockStmt) { + return ws->else_stmt->BlockStmt.stmts; + } + return {}; +} + +gb_internal void collect_global_decl_source_stmts(CheckerContext *ctx, Slice const &stmts) { + AstFile *f = ctx->file; + for (Ast *decl : stmts) { + if (decl->kind == Ast_ValueDecl) { + check_collect_value_decl(ctx, decl); + } + } + check_export_entities_in_pkg(&global_decl_source_export_ctx, f->pkg, &global_decl_source_export_untyped); + + // NOTE(bill): after the value declarations, as their attributes are evaluated + for (Ast *decl : stmts) { + switch (decl->kind) { + case_ast_node(fl, ForeignImportDecl, decl); + check_add_foreign_import_decl(ctx, decl); + case_end; + + case_ast_node(es, ExprStmt, decl); + if (es->expr->kind == Ast_CallExpr && es->expr->CallExpr.proc->kind == Ast_BasicDirective && + (decl->state_flags & StateFlag_BeenHandled) == 0) { + decl->state_flags |= StateFlag_BeenHandled; + array_add(&f->delayed_decls_queues[AstDelayQueue_Expr], es->expr); + } + case_end; + } + } +} + +gb_internal Token global_decl_source_token(GlobalDeclSource *src) { + if (src->node->kind == Ast_WhenStmt) { + return src->node->WhenStmt.token; + } + return src->node->ForeignBlockDecl.token; +} + +gb_internal void report_global_decl_source_cycle(GlobalDeclSource *src, InternedString needed) { + if (src->reported_cycle) { + return; + } + src->reported_cycle = true; + + isize start = 0; + for (isize i = global_decl_source_stack.count-1; i >= 0; i--) { + if (global_decl_source_stack[i].source == src) { + start = i; + break; + } + } + + ERROR_BLOCK(); + Token token = global_decl_source_token(src); + error(token, "Cyclic dependency between global '%.*s' declarations", LIT(token.string)); + for (isize i = start; i < global_decl_source_stack.count; i++) { + Token t = global_decl_source_token(global_decl_source_stack[i].source); + InternedString name = i+1 < global_decl_source_stack.count ? global_decl_source_stack[i].needs : needed; + error_line("\t'%.*s' at %s needs '%s', which may be declared by\n", LIT(t.string), token_pos_to_string(t.pos), name.cstring()); + } + error_line("\t'%.*s' at %s\n", LIT(token.string), token_pos_to_string(token.pos)); +} + +gb_internal void resolve_global_decl_source(GlobalDeclSource *src, InternedString needed) { + if (src->state == EntityState_Resolved) { + return; + } + if (src->state == EntityState_InProgress) { + report_global_decl_source_cycle(src, needed); + return; + } + + GlobalDeclSource *foreign_block = nullptr; + if (src->parent != nullptr) { + GlobalDeclSource *parent = src->parent; + resolve_global_decl_source(parent, needed); + if (parent->state != EntityState_Resolved) { + return; + } + bool reachable = parent->reachable; + if (parent->node->kind == Ast_WhenStmt) { + reachable = reachable && parent->node->WhenStmt.determined_cond != src->in_else; + } + if (!reachable) { + src->reachable = false; + src->state = EntityState_Resolved; + return; + } + for (GlobalDeclSource *p = parent; p != nullptr; p = p->parent) { + if (p->node->kind == Ast_ForeignBlockDecl) { + foreign_block = p; + break; + } + } + } + + src->state = EntityState_InProgress; + array_add(&global_decl_source_stack, GlobalDeclSourceFrame{src, {}}); + + CheckerContext ctx = {}; + init_checker_context(&ctx, global_checker_ptr.load(std::memory_order_relaxed)); + UntypedExprInfoMap untyped = {}; + reset_checker_context(&ctx, src->file, &untyped); + if (foreign_block != nullptr) { + ctx.foreign_context = foreign_block->foreign_context; + } + + if (src->node->kind == Ast_WhenStmt) { + ast_node(ws, WhenStmt, src->node); + Operand operand = {Addressing_Invalid}; + check_expr(&ctx, &operand, ws->cond); + if (operand.mode != Addressing_Invalid && !is_type_boolean(operand.type)) { + error(ws->cond, "Non-boolean condition in 'when' statement"); + } + if (operand.mode != Addressing_Constant) { + error(ws->cond, "Non-constant condition in 'when' statement"); + } + ws->is_cond_determined = true; + ws->determined_cond = operand.value.kind == ExactValue_Bool && operand.value.value_bool; + if (ws->body == nullptr || ws->body->kind != Ast_BlockStmt) { + error(ws->cond, "Invalid body for 'when' statement"); + } else if (ws->else_stmt != nullptr && ws->else_stmt->kind != Ast_BlockStmt && ws->else_stmt->kind != Ast_WhenStmt) { + error(ws->else_stmt, "Invalid 'else' statement in 'when' statement"); + } + } else { + ast_node(fb, ForeignBlockDecl, src->node); + if (fb->foreign_library->kind == Ast_Ident) { + ctx.foreign_context.curr_library = fb->foreign_library; + } else { + error(fb->foreign_library, "Foreign block name must be an identifier or 'export'"); + ctx.foreign_context.curr_library = nullptr; + } + check_decl_attributes(&ctx, fb->attributes, foreign_block_decl_attribute, nullptr); + src->foreign_context = ctx.foreign_context; + } + + // NOTE: resolved before its declarations are collected, which evaluates the attributes of 'foreign import's + src->state = EntityState_Resolved; + array_pop(&global_decl_source_stack); + + collect_global_decl_source_stmts(&ctx, global_decl_source_taken_stmts(src)); + + add_untyped_expressions(ctx.info, &untyped); + map_destroy(&untyped); + destroy_checker_context(&ctx); +} + +gb_internal Entity *force_scope_placeholders(Scope *s, InternedString name, u32 hash) { + PtrMap *m = s->placeholders; + bool forced = false; + for (auto *e = multi_map_find_first(m, cast(u64)name.value); e != nullptr; e = multi_map_find_next(m, e)) { + GlobalDeclSource *src = e->value; + if (src->state != EntityState_Resolved) { + if (global_decl_source_stack.count > 0) { + global_decl_source_stack[global_decl_source_stack.count-1].needs = name; + } + resolve_global_decl_source(src, name); + forced = true; + } + } + if (!forced) { + return nullptr; + } + rw_mutex_shared_lock(&s->mutex); + Entity *found = scope_map_get(&s->elements, name, hash); + rw_mutex_shared_unlock(&s->mutex); + return found; +} + +gb_internal void check_vet_when_shadowing_entity(Entity *e) { + if (e == nullptr || e->scope == nullptr || (e->scope->flags & ScopeFlag_File) == 0) { + return; + } + InternedString name = entity_interned_name(e); + u32 hash = e->interned_name_hash.load(std::memory_order_relaxed); + Scope *outer = e->scope->parent; + if (scope_map_get(&e->scope->elements, name, hash) != e) { + outer = outer->parent; // in the package scope + } + if (outer == nullptr) { + return; + } + Entity *shadowed = scope_lookup(outer, name, hash); + if (shadowed == nullptr || shadowed == e) { + return; + } + if (shadowed->scope == builtin_pkg->scope) { + error(e->token, "Declaration of '%.*s' within a global 'when' shadows the builtin '%.*s'", LIT(e->token.string), LIT(e->token.string)); + } else { + error(e->token, "Declaration of '%.*s' within a global 'when' shadows the declaration at %s", LIT(e->token.string), token_pos_to_string(shadowed->token.pos)); + } +} + +gb_internal void check_vet_when_shadowing(void) { + for (GlobalDeclSource *src : global_decl_sources) { + if (!src->reachable || src->state != EntityState_Resolved) { + continue; + } + if ((ast_file_vet_flags(src->file) & VetFlag_WhenShadowing) == 0 || !is_global_decl_source_in_when(src)) { + continue; + } + for (Ast *decl : global_decl_source_taken_stmts(src)) { + if (decl->kind == Ast_ValueDecl) { + for (Ast *name : decl->ValueDecl.names) { + if (name->kind == Ast_Ident) { + check_vet_when_shadowing_entity(name->Ident.entity.load()); + } + } + } else if (decl->kind == Ast_ForeignImportDecl) { + Token token = decl->ForeignImportDecl.library_name; + InternedString name = string_interner_insert(token.string); + for (Scope *s = src->file->scope; s != nullptr && s != builtin_pkg->scope; s = s->parent) { + Entity *e = scope_map_get(&s->elements, name, name.hash()); + if (e != nullptr && e->kind == Entity_LibraryName && e->LibraryName.decl == decl) { + check_vet_when_shadowing_entity(e); + break; + } + } + } + } + } +} + +// Placeholders for every file, then every source resolved in package, file and source order, which +// only matters for which errors are reported +gb_internal void resolve_global_decl_sources(Checker *c, Array const &package_order) { + array_init(&global_decl_sources, heap_allocator()); + array_init(&global_decl_source_stack, heap_allocator()); + array_init(&global_placeholder_scopes, heap_allocator()); + init_checker_context(&global_decl_source_export_ctx, c); + defer (destroy_checker_context(&global_decl_source_export_ctx)); + + u64 stage_start = global_import_stage_begin(); + for (ImportGraphNode *node : package_order) { + for (AstFile *f : node->pkg->files) { + scan_global_decl_sources(f, f->decls, nullptr, false, EntityVisiblity_Public); + } + } + global_import_stage_end(GlobalImportStage_Placeholders, stage_start); + + stage_start = global_import_stage_begin(); + for (GlobalDeclSource *src : global_decl_sources) { + resolve_global_decl_source(src, {}); + } + GB_ASSERT(global_decl_source_stack.count == 0); + + for (Scope *s : global_placeholder_scopes) { + map_destroy(s->placeholders); + s->placeholders = nullptr; + } + array_clear(&global_placeholder_scopes); + + check_vet_when_shadowing(); + global_import_stage_end(GlobalImportStage_DeclSources, stage_start); + + map_destroy(&global_decl_source_export_untyped); +} + + +// Global entities are checked in groups: the strongly connected components of a dependency graph built +// from syntax, in dependency order. A group only names entities of its own or of finished groups, which +// `-internal-check-global-edges` verifies + +// Iterative Tarjan; components are numbered so that every edge v->w has comp(w) <= comp(v) +gb_internal i32 global_graph_scc(i32 node_count, Array const &offsets, Array const &targets, Array *comp_of_) { + struct Frame { + i32 v; + i32 pos; + }; + + auto index = array_make(heap_allocator(), node_count); + auto low = array_make(heap_allocator(), node_count); + auto on_stack = array_make(heap_allocator(), node_count); + auto stack = array_make(heap_allocator(), 0, node_count); + auto frames = array_make(heap_allocator(), 0, 64); + defer (array_free(&index)); + defer (array_free(&low)); + defer (array_free(&on_stack)); + defer (array_free(&stack)); + defer (array_free(&frames)); + + Array &comp_of = *comp_of_; + for (i32 v = 0; v < node_count; v++) { + index[v] = -1; + low[v] = 0; + on_stack[v] = false; + comp_of[v] = -1; + } + + i32 counter = 0; + i32 comp_count = 0; + for (i32 root = 0; root < node_count; root++) { + if (index[root] >= 0) { + continue; + } + index[root] = low[root] = counter++; + array_add(&stack, root); + on_stack[root] = true; + array_add(&frames, Frame{root, 0}); + + while (frames.count > 0) { + Frame *top = &frames[frames.count-1]; + i32 v = top->v; + if (offsets[v] + top->pos < offsets[v+1]) { + i32 w = targets[offsets[v] + top->pos]; + top->pos += 1; + if (index[w] < 0) { + index[w] = low[w] = counter++; + array_add(&stack, w); + on_stack[w] = true; + array_add(&frames, Frame{w, 0}); + } else if (on_stack[w]) { + low[v] = gb_min(low[v], index[w]); + } + continue; + } + + if (low[v] == index[v]) { + for (;;) { + i32 w = array_pop(&stack); + on_stack[w] = false; + comp_of[w] = comp_count; + if (w == v) { + break; + } + } + comp_count += 1; + } + array_pop(&frames); + if (frames.count > 0) { + i32 u = frames[frames.count-1].v; + low[u] = gb_min(low[u], low[v]); + } + } + } + return comp_count; +} + +gb_internal void global_graph_print_entity(Entity *e) { + if (e == nullptr) { + gb_printf_err("?"); + return; + } + String pkg = e->pkg ? e->pkg->name : str_lit("?"); + String file = e->file ? filename_without_directory(e->file->fullpath) : str_lit("?"); + gb_printf_err("%.*s.%.*s (%.*s:%d)", LIT(pkg), LIT(e->token.string), LIT(file), e->token.pos.line); +} + + + + +struct GlobalGroup { + i32 start; // into `GlobalGroupGraph::members` + i32 count; + bool done; +}; + +struct GlobalGroupGraph { + Array nodes; + PtrMap node_of; + Array offsets; // node -> the nodes it names, as `targets[offsets[v]..offsets[v+1]]` + Array targets; + Array group_of; + Array groups; // every dependency of a group has a lower index + Array members; // nodes, by group, in source order + + bool active; + i32 current_group; + Entity *current_entity; + isize missing_edges; +}; + +gb_global GlobalGroupGraph global_groups; + +struct GlobalGraphWalk { + Scope *scope; + Array *refs; +}; + +gb_internal void global_graph_walk(GlobalGraphWalk *w, Ast *node); + +gb_internal void global_graph_walk_slice(GlobalGraphWalk *w, Slice const &nodes) { + for (Ast *node : nodes) { + global_graph_walk(w, node); + } +} + +gb_internal void global_graph_add_ref(GlobalGraphWalk *w, Entity *e) { + if (e != nullptr) { + array_add(w->refs, e); + } +} + +// NOTE: names bound within the expression (parameters, fields, '$T') are also looked up globally, which at +// worst adds an edge; only procedure bodies are skipped, as they are checked after this stage +gb_internal void global_graph_walk(GlobalGraphWalk *w, Ast *node) { + if (node == nullptr) { + return; + } + switch (node->kind) { + case Ast_Ident: + global_graph_add_ref(w, scope_lookup(w->scope, node->Ident.interned, node->Ident.hash)); + break; + + case Ast_SelectorExpr: { + Ast *expr = node->SelectorExpr.expr; + Ast *selector = node->SelectorExpr.selector; + if (expr != nullptr && expr->kind == Ast_Ident) { + Entity *e = scope_lookup(w->scope, expr->Ident.interned, expr->Ident.hash); + if (e != nullptr && e->kind == Entity_ImportName && selector != nullptr && selector->kind == Ast_Ident) { + global_graph_add_ref(w, scope_lookup_current(e->ImportName.scope, selector->Ident.interned, selector->Ident.hash)); + } else { + global_graph_add_ref(w, e); + } + } else { + global_graph_walk(w, expr); + } + } break; + + case Ast_PolyType: + global_graph_walk(w, node->PolyType.specialization); + break; + case Ast_Ellipsis: + global_graph_walk(w, node->Ellipsis.expr); + break; + case Ast_ProcGroup: + global_graph_walk_slice(w, node->ProcGroup.args); + break; + case Ast_AsmGroup: + global_graph_walk_slice(w, node->AsmGroup.args); + break; + case Ast_ProcLit: + global_graph_walk(w, node->ProcLit.type); + global_graph_walk_slice(w, node->ProcLit.where_clauses); + break; + case Ast_CompoundLit: + global_graph_walk(w, node->CompoundLit.type); + global_graph_walk_slice(w, node->CompoundLit.elems); + global_graph_walk(w, node->CompoundLit.tag); + break; + case Ast_TagExpr: + global_graph_walk(w, node->TagExpr.expr); + break; + case Ast_UnaryExpr: + global_graph_walk(w, node->UnaryExpr.expr); + break; + case Ast_BinaryExpr: + global_graph_walk(w, node->BinaryExpr.left); + global_graph_walk(w, node->BinaryExpr.right); + break; + case Ast_ParenExpr: + global_graph_walk(w, node->ParenExpr.expr); + break; + case Ast_SelectorCallExpr: + global_graph_walk(w, node->SelectorCallExpr.expr); + global_graph_walk(w, node->SelectorCallExpr.call); + break; + case Ast_IndexExpr: + global_graph_walk(w, node->IndexExpr.expr); + global_graph_walk(w, node->IndexExpr.index); + break; + case Ast_MatrixIndexExpr: + global_graph_walk(w, node->MatrixIndexExpr.expr); + global_graph_walk(w, node->MatrixIndexExpr.row_index); + global_graph_walk(w, node->MatrixIndexExpr.column_index); + break; + case Ast_DerefExpr: + global_graph_walk(w, node->DerefExpr.expr); + break; + case Ast_SliceExpr: + global_graph_walk(w, node->SliceExpr.expr); + global_graph_walk(w, node->SliceExpr.low); + global_graph_walk(w, node->SliceExpr.high); + break; + case Ast_CallExpr: + global_graph_walk(w, node->CallExpr.proc); + global_graph_walk_slice(w, node->CallExpr.args); + break; + case Ast_FieldValue: + global_graph_walk(w, node->FieldValue.field); + global_graph_walk(w, node->FieldValue.value); + break; + case Ast_EnumFieldValue: + global_graph_walk(w, node->EnumFieldValue.value); + break; + case Ast_TernaryIfExpr: + global_graph_walk(w, node->TernaryIfExpr.x); + global_graph_walk(w, node->TernaryIfExpr.cond); + global_graph_walk(w, node->TernaryIfExpr.y); + break; + case Ast_TernaryWhenExpr: + global_graph_walk(w, node->TernaryWhenExpr.x); + global_graph_walk(w, node->TernaryWhenExpr.cond); + global_graph_walk(w, node->TernaryWhenExpr.y); + break; + case Ast_OrElseExpr: + global_graph_walk(w, node->OrElseExpr.x); + global_graph_walk(w, node->OrElseExpr.y); + break; + case Ast_OrReturnExpr: + global_graph_walk(w, node->OrReturnExpr.expr); + break; + case Ast_OrBranchExpr: + global_graph_walk(w, node->OrBranchExpr.expr); + break; + case Ast_TypeAssertion: + global_graph_walk(w, node->TypeAssertion.expr); + global_graph_walk(w, node->TypeAssertion.type); + break; + case Ast_TypeCast: + global_graph_walk(w, node->TypeCast.type); + global_graph_walk(w, node->TypeCast.expr); + break; + case Ast_AutoCast: + global_graph_walk(w, node->AutoCast.expr); + break; + + case Ast_Field: + global_graph_walk(w, node->Field.type); + global_graph_walk(w, node->Field.default_value); + break; + case Ast_BitFieldField: + global_graph_walk(w, node->BitFieldField.type); + global_graph_walk(w, node->BitFieldField.bit_size); + break; + case Ast_FieldList: + global_graph_walk_slice(w, node->FieldList.list); + break; + + case Ast_TypeidType: + global_graph_walk(w, node->TypeidType.specialization); + break; + case Ast_HelperType: + global_graph_walk(w, node->HelperType.type); + break; + case Ast_DistinctType: + global_graph_walk(w, node->DistinctType.type); + break; + case Ast_ProcType: + global_graph_walk(w, node->ProcType.params); + global_graph_walk(w, node->ProcType.results); + break; + case Ast_RelativeType: + global_graph_walk(w, node->RelativeType.tag); + global_graph_walk(w, node->RelativeType.type); + break; + case Ast_PointerType: + global_graph_walk(w, node->PointerType.type); + global_graph_walk(w, node->PointerType.tag); + break; + case Ast_MultiPointerType: + global_graph_walk(w, node->MultiPointerType.type); + break; + case Ast_ArrayType: + global_graph_walk(w, node->ArrayType.count); + global_graph_walk(w, node->ArrayType.elem); + global_graph_walk(w, node->ArrayType.tag); + break; + case Ast_DynamicArrayType: + global_graph_walk(w, node->DynamicArrayType.elem); + global_graph_walk(w, node->DynamicArrayType.tag); + break; + case Ast_FixedCapacityDynamicArrayType: + global_graph_walk(w, node->FixedCapacityDynamicArrayType.elem); + global_graph_walk(w, node->FixedCapacityDynamicArrayType.capacity); + global_graph_walk(w, node->FixedCapacityDynamicArrayType.tag); + break; + case Ast_StructType: + global_graph_walk_slice(w, node->StructType.fields); + global_graph_walk(w, node->StructType.polymorphic_params); + global_graph_walk(w, node->StructType.align); + global_graph_walk(w, node->StructType.min_field_align); + global_graph_walk(w, node->StructType.max_field_align); + global_graph_walk_slice(w, node->StructType.where_clauses); + break; + case Ast_UnionType: + global_graph_walk_slice(w, node->UnionType.variants); + global_graph_walk(w, node->UnionType.polymorphic_params); + global_graph_walk_slice(w, node->UnionType.where_clauses); + break; + case Ast_EnumType: + global_graph_walk(w, node->EnumType.base_type); + for (Ast *field : node->EnumType.fields) { + if (field->kind == Ast_EnumFieldValue) { + global_graph_walk(w, field->EnumFieldValue.value); + } + } + break; + case Ast_BitSetType: + global_graph_walk(w, node->BitSetType.elem); + global_graph_walk(w, node->BitSetType.underlying); + break; + case Ast_BitFieldType: + global_graph_walk(w, node->BitFieldType.backing_type); + global_graph_walk_slice(w, node->BitFieldType.fields); + break; + case Ast_MapType: + global_graph_walk(w, node->MapType.count); + global_graph_walk(w, node->MapType.key); + global_graph_walk(w, node->MapType.value); + break; + case Ast_MatrixType: + global_graph_walk(w, node->MatrixType.row_count); + global_graph_walk(w, node->MatrixType.column_count); + global_graph_walk(w, node->MatrixType.elem); + break; + + case Ast_AsmTemplate: + global_graph_walk(w, node->AsmTemplate.signature); + global_graph_walk_slice(w, node->AsmTemplate.specs); + global_graph_walk_slice(w, node->AsmTemplate.clobbers); + global_graph_walk_slice(w, node->AsmTemplate.instructions); + break; + case Ast_AsmSpec: + global_graph_walk(w, node->AsmSpec.type); + global_graph_walk(w, node->AsmSpec.value); + for (Ast *d : node->AsmSpec.directives) { + global_graph_walk(w, d); + } + break; + case Ast_AsmClobber: + global_graph_walk(w, node->AsmClobber.value); + break; + case Ast_AsmInstruction: + global_graph_walk_slice(w, node->AsmInstruction.operands); + break; + case Ast_AsmMemoryTerm: + global_graph_walk(w, node->AsmMemoryTerm.operand); + global_graph_walk(w, node->AsmMemoryTerm.scale); + break; + case Ast_AsmMemoryOperand: + global_graph_walk(w, node->AsmMemoryOperand.segment_override); + global_graph_walk_slice(w, node->AsmMemoryOperand.terms); + global_graph_walk(w, node->AsmMemoryOperand.type); + break; + case Ast_AsmRegisterGroup: + for (Ast *r : node->AsmRegisterGroup.registers) { + global_graph_walk(w, r); + } + global_graph_walk(w, node->AsmRegisterGroup.type); + break; + case Ast_AsmDirective: + global_graph_walk_slice(w, node->AsmDirective.operands); + break; + } +} + +gb_internal void global_graph_walk_entity(GlobalGraphWalk *w, Entity *e, DeclInfo *d) { + w->scope = d->scope; + global_graph_walk(w, d->type_expr); + global_graph_walk(w, d->init_expr); + for (Ast *attr : d->attributes) { + if (attr->kind != Ast_Attribute) { + continue; + } + for (Ast *elem : attr->Attribute.elems) { + if (elem->kind == Ast_FieldValue) { + global_graph_walk(w, elem->FieldValue.value); + } + } + } + if (e->kind == Entity_Procedure) { + global_graph_walk(w, e->Procedure.foreign_library_ident); + } else if (e->kind == Entity_Variable) { + global_graph_walk(w, e->Variable.foreign_library_ident); + } +} + +gb_internal bool is_global_graph_node(Entity *e) { + if (e->state == EntityState_Resolved) { + return false; + } + DeclInfo *d = e->decl_info; + if (d == nullptr || e->scope == nullptr || d->scope != e->scope || (e->scope->flags & ScopeFlag_File) == 0) { + return false; + } + switch (e->kind) { + case Entity_Constant: + case Entity_TypeName: + case Entity_Variable: + case Entity_Procedure: + case Entity_ProcGroup: + case Entity_AsmTemplate: + return true; + } + return false; +} + +gb_internal i32 global_graph_add_node(GlobalGroupGraph *g, Entity *e) { + i32 *found = map_get(&g->node_of, e); + if (found != nullptr) { + return *found; + } + i32 v = cast(i32)g->nodes.count; + map_set(&g->node_of, e, v); + array_add(&g->nodes, e); + return v; +} + +gb_internal u64 global_group_random(u64 *state) { + *state = *state*6364136223846793005ull + 1442695040888963407ull; + return *state >> 33; +} + +// The nodes `[lo, hi)` walked on one thread; a name that is not a node yet is kept as an entity in `refs` +struct GlobalGraphWalkChunk { + GlobalGroupGraph *g; + i32 lo; + i32 hi; + Array targets; + Array target_ends; // per node + Array refs; + Array ref_ends; // per node +}; + +gb_internal WORKER_TASK_PROC(global_graph_walk_worker) { + GlobalGraphWalkChunk *chunk = cast(GlobalGraphWalkChunk *)data; + GlobalGroupGraph *g = chunk->g; + + auto refs = array_make(heap_allocator(), 0, 64); + defer (array_free(&refs)); + GlobalGraphWalk w = {}; + w.refs = &refs; + for (i32 v = chunk->lo; v < chunk->hi; v++) { + Entity *e = g->nodes[v]; + array_clear(&refs); + global_graph_walk_entity(&w, e, e->decl_info); + for (Entity *r : refs) { + i32 *found = map_get(&g->node_of, r); + if (found != nullptr) { + array_add(&chunk->targets, *found); + } else if (r->flags & EntityFlag_Lazy) { + array_add(&chunk->refs, r); + } + } + array_add(&chunk->target_ends, cast(i32)chunk->targets.count); + array_add(&chunk->ref_ends, cast(i32)chunk->refs.count); + } + return 0; +} + +gb_internal void build_global_groups(Checker *c, GlobalGroupGraph *g) { + array_init(&g->nodes, heap_allocator(), 0, c->info.entities.count); + map_init(&g->node_of, c->info.entities.count); + for (Entity *e : c->info.entities) { + if ((e->flags & EntityFlag_Lazy) == 0 && is_global_graph_node(e)) { + global_graph_add_node(g, e); + } + } + + // NOTE: walked in parallel, as nothing writes to the scopes now + i32 const CHUNK_SIZE = 64; + i32 initial_count = cast(i32)g->nodes.count; + auto chunks = array_make(heap_allocator(), (initial_count + CHUNK_SIZE-1)/CHUNK_SIZE); + defer (array_free(&chunks)); + for (isize i = 0; i < chunks.count; i++) { + GlobalGraphWalkChunk *chunk = &chunks[i]; + *chunk = {}; + chunk->g = g; + chunk->lo = cast(i32)(i*CHUNK_SIZE); + chunk->hi = gb_min(chunk->lo + CHUNK_SIZE, initial_count); + array_init(&chunk->targets, heap_allocator(), 0, 4*CHUNK_SIZE); + array_init(&chunk->target_ends, heap_allocator(), 0, CHUNK_SIZE); + array_init(&chunk->refs, heap_allocator(), 0); + array_init(&chunk->ref_ends, heap_allocator(), 0, CHUNK_SIZE); + thread_pool_add_task(global_graph_walk_worker, chunk); + } + thread_pool_wait(); + + auto edge_from = array_make(heap_allocator(), 0, 4*g->nodes.count); + auto edge_to = array_make(heap_allocator(), 0, 4*g->nodes.count); + auto refs = array_make(heap_allocator(), 0, 64); + defer (array_free(&edge_from)); + defer (array_free(&edge_to)); + defer (array_free(&refs)); + + auto add_ref = [&](i32 v, Entity *r) { + i32 *found = map_get(&g->node_of, r); + if (found != nullptr) { + array_add(&edge_from, v); + array_add(&edge_to, *found); + } else if ((r->flags & EntityFlag_Lazy) && is_global_graph_node(r)) { + // NOTE: a lazy entity becomes a node once a node names it + array_add(&edge_from, v); + array_add(&edge_to, global_graph_add_node(g, r)); + } + }; + + GlobalGraphWalk w = {}; + w.refs = &refs; + i32 first_of_decl = -1; + for (i32 v = 0; v < g->nodes.count; v++) { + Entity *e = g->nodes[v]; + DeclInfo *d = e->decl_info; + + // NOTE: entities sharing one declaration share its AST, e.g. `a, b: struct{x: int}`, so they share a group; + // in source order they are adjacent, and lazy ones are only checked under `lazy_mutex` + if (first_of_decl >= 0 && d->decl_node != nullptr && g->nodes[first_of_decl]->decl_info->decl_node == d->decl_node) { + array_add(&edge_from, v); + array_add(&edge_to, first_of_decl); + array_add(&edge_from, first_of_decl); + array_add(&edge_to, v); + } else { + first_of_decl = v; + } + + if (v < initial_count) { + GlobalGraphWalkChunk *chunk = &chunks[v / CHUNK_SIZE]; + i32 k = v - chunk->lo; + for (i32 i = k > 0 ? chunk->target_ends[k-1] : 0; i < chunk->target_ends[k]; i++) { + array_add(&edge_from, v); + array_add(&edge_to, chunk->targets[i]); + } + for (i32 i = k > 0 ? chunk->ref_ends[k-1] : 0; i < chunk->ref_ends[k]; i++) { + add_ref(v, chunk->refs[i]); + } + } else { + array_clear(&refs); + global_graph_walk_entity(&w, e, d); + for (Entity *r : refs) { + add_ref(v, r); + } + } + } + for (GlobalGraphWalkChunk &chunk : chunks) { + array_free(&chunk.targets); + array_free(&chunk.target_ends); + array_free(&chunk.refs); + array_free(&chunk.ref_ends); + } + + i32 node_count = cast(i32)g->nodes.count; + array_init(&g->offsets, heap_allocator(), node_count+1); + array_init(&g->targets, heap_allocator(), edge_to.count); + for (i32 v = 0; v <= node_count; v++) { + g->offsets[v] = 0; + } + for (i32 from : edge_from) { + g->offsets[from+1] += 1; + } + for (i32 v = 0; v < node_count; v++) { + g->offsets[v+1] += g->offsets[v]; + } + { + auto fill = array_clone(heap_allocator(), g->offsets); + defer (array_free(&fill)); + for (isize i = 0; i < edge_from.count; i++) { + g->targets[fill[edge_from[i]]++] = edge_to[i]; + } + } + + array_init(&g->group_of, heap_allocator(), node_count); + i32 group_count = global_graph_scc(node_count, g->offsets, g->targets, &g->group_of); + + array_init(&g->groups, heap_allocator(), group_count); + array_init(&g->members, heap_allocator(), node_count); + for (i32 gi = 0; gi < group_count; gi++) { + g->groups[gi] = {}; + } + for (i32 v = 0; v < node_count; v++) { + g->groups[g->group_of[v]].count += 1; + } + i32 start = 0; + for (i32 gi = 0; gi < group_count; gi++) { + g->groups[gi].start = start; + start += g->groups[gi].count; + g->groups[gi].count = 0; + } + // NOTE: in source order, as `c->info.entities` is sorted; lazy nodes come last, but are not checked here + for (i32 v = 0; v < node_count; v++) { + GlobalGroup *group = &g->groups[g->group_of[v]]; + g->members[group->start + group->count++] = v; + } +} + +// Called when `e` starts being checked: it must be in the current group or a finished one +gb_internal void global_group_check_edge(CheckerContext *ctx, Entity *e) { + GlobalGroupGraph *g = &global_groups; + if (!g->active) { + return; + } + i32 *v = map_get(&g->node_of, e); + if (v == nullptr) { + if (!is_global_graph_node(e)) { + return; + } + } else { + i32 gi = g->group_of[*v]; + if (gi == g->current_group || g->groups[gi].done) { + return; + } + } + + g->missing_edges += 1; + if (build_context.internal_check_global_edges) { + Entity *by = ctx->decl ? ctx->decl->entity.load() : nullptr; + gb_printf_err("Missing global dependency: "); + global_graph_print_entity(by); + gb_printf_err(" needs "); + global_graph_print_entity(e); + gb_printf_err(v == nullptr ? ", which is not in the graph" : ""); + if (g->current_entity != by) { + gb_printf_err(", while checking "); + global_graph_print_entity(g->current_entity); + } + gb_printf_err("\n"); + } +} + +// NOTE: members in a fixed order, as which member of a cycle is entered first can decide whether it checks, +// e.g. an enum whose values are `union_variant_index`es of a union with pointers back to it +gb_internal void check_global_group(Checker *c, GlobalGroupGraph *g, i32 gi) { + GlobalGroup *group = &g->groups[gi]; + i32 *members = g->members.data + group->start; + + g->current_group = gi; + for (i32 k = 0; k < group->count; k++) { + Entity *e = g->nodes[members[k]]; + if (e->flags & EntityFlag_Lazy) { + // NOTE: only checked when something uses it; the group orders it after what it names + continue; + } + g->current_entity = e; + GlobalEntityTimingFrame timing_frame = global_entity_timing_begin(e); + check_single_global_entity(c, e, e->decl_info); + if (e->type != nullptr && is_type_typed(e->type)) { + for (Type *t = nullptr; mpsc_dequeue(&c->soa_types_to_complete, &t); /**/) { + complete_soa_type(c, t, false); + } + + (void)type_size_of(e->type); + (void)type_align_of(e->type); + } + global_entity_timing_end(timing_frame, e); + } + group->done = true; + g->current_group = -1; + g->current_entity = nullptr; +} + +// Groups in dependency order; with `-internal-shuffle-global-entities`, a random one of the groups whose +// dependencies are done, as a parallel checker might +gb_internal void check_global_groups(Checker *c, GlobalGroupGraph *g) { + i32 group_count = cast(i32)g->groups.count; + u64 seed = build_context.internal_shuffle_global_entities; + if (seed == 0) { + for (i32 gi = 0; gi < group_count; gi++) { + check_global_group(c, g, gi); + } + return; + } + + auto dependents = array_make >(heap_allocator(), group_count); + auto dep_count = array_make (heap_allocator(), group_count); + auto seen = array_make (heap_allocator(), group_count); + auto ready = array_make (heap_allocator(), 0, group_count); + defer ({ + for (auto &d : dependents) { + array_free(&d); + } + array_free(&dependents); + }); + defer (array_free(&dep_count)); + defer (array_free(&seen)); + defer (array_free(&ready)); + + for (i32 gi = 0; gi < group_count; gi++) { + dep_count[gi] = 0; + dependents[gi] = {}; + seen[gi] = -1; + } + for (i32 gi = 0; gi < group_count; gi++) { + GlobalGroup const &group = g->groups[gi]; + for (i32 k = 0; k < group.count; k++) { + i32 v = g->members[group.start + k]; + for (i32 i = g->offsets[v]; i < g->offsets[v+1]; i++) { + i32 dep = g->group_of[g->targets[i]]; + if (dep != gi && seen[dep] != gi) { + seen[dep] = gi; + dep_count[gi] += 1; + if (dependents[dep].allocator.proc == nullptr) { + array_init(&dependents[dep], heap_allocator()); + } + array_add(&dependents[dep], gi); + } + } + } + if (dep_count[gi] == 0) { + array_add(&ready, gi); + } + } + + u64 state = seed; + isize checked = 0; + while (ready.count > 0) { + isize i = cast(isize)(global_group_random(&state) % cast(u64)ready.count); + i32 gi = ready[i]; + ready[i] = ready[ready.count-1]; + array_pop(&ready); + + check_global_group(c, g, gi); + checked += 1; + for (i32 next : dependents[gi]) { + if (--dep_count[next] == 0) { + array_add(&ready, next); + } + } + } + GB_ASSERT(checked == group_count); +} + +gb_internal void destroy_global_groups(GlobalGroupGraph *g) { + array_free(&g->nodes); + map_destroy(&g->node_of); + array_free(&g->offsets); + array_free(&g->targets); + array_free(&g->group_of); + array_free(&g->groups); + array_free(&g->members); +} + +gb_internal void check_all_global_entities(Checker *c) { + in_single_threaded_checker_stage.store(true, std::memory_order_relaxed); + + // NOTE(bill): the runtime types the checker looks up by name rather than through a declaration + init_preload(c); + { + u32 hash = 0; + InternedString name = string_interner_insert(str_lit("Load_Directory_File"), 0, &hash); + if (scope_lookup_current(c->info.runtime_package->scope, name, hash) != nullptr) { + init_core_load_directory_file(c); + } + } + + TIME_SECTION("check all global entities - build groups"); + GlobalGroupGraph *g = &global_groups; + build_global_groups(c, g); + + TIME_SECTION("check all global entities - check groups"); + g->active = true; + g->current_group = -1; + check_global_groups(c, g); + g->active = false; + + if (build_context.internal_check_global_edges && g->missing_edges > 0) { + gb_printf_err("%td missing global dependencies\n", g->missing_edges); + gb_exit(1); + } + + in_single_threaded_checker_stage.store(false, std::memory_order_relaxed); +} + + +// -internal-global-entity-graph: the groups, weighted by the measured self time of their entities + +struct GlobalGraphSortItem { + u64 key; + i32 id; +}; + +gb_internal GB_COMPARE_PROC(global_graph_sort_item_desc) { + GlobalGraphSortItem const *x = cast(GlobalGraphSortItem const *)a; + GlobalGraphSortItem const *y = cast(GlobalGraphSortItem const *)b; + if (x->key != y->key) { + return x->key > y->key ? -1 : +1; + } + return i32_cmp(x->id, y->id); +} + +gb_internal f64 global_graph_ms(u64 ticks, u64 freq) { + return 1000.0 * cast(f64)ticks / cast(f64)freq; +} + +gb_internal void print_global_group(GlobalGroupGraph *g, i32 gi, u64 ticks, u64 freq, isize max_names) { + GlobalGroup const &group = g->groups[gi]; + gb_printf_err(" %10.3f ms %7d entities ", global_graph_ms(ticks, freq), group.count); + for (i32 k = 0; k < group.count && k < max_names; k++) { + if (k > 0) { + gb_printf_err(", "); + } + global_graph_print_entity(g->nodes[g->members[group.start + k]]); + } + if (group.count > max_names) { + gb_printf_err(", ..."); + } + gb_printf_err("\n"); +} + +gb_internal void print_global_groups(GlobalGroupGraph *g) { + u64 const freq = time_stamp__freq(); + i32 group_count = cast(i32)g->groups.count; + + auto ticks = array_make(heap_allocator(), group_count); + auto path = array_make(heap_allocator(), group_count); // heaviest chain of dependencies ending at a group + auto len = array_make(heap_allocator(), group_count); + auto prev = array_make(heap_allocator(), group_count); + auto seen = array_make(heap_allocator(), group_count); + defer (array_free(&ticks)); + defer (array_free(&path)); + defer (array_free(&len)); + defer (array_free(&prev)); + defer (array_free(&seen)); + + auto pkgs = array_make(heap_allocator(), 0, 64); + auto pkg_ticks = array_make(heap_allocator(), 0, 64); + auto pkg_entities = array_make(heap_allocator(), 0, 64); + PtrMap pkg_index = {}; + map_init(&pkg_index); + defer (array_free(&pkgs)); + defer (array_free(&pkg_ticks)); + defer (array_free(&pkg_entities)); + defer (map_destroy(&pkg_index)); + + u64 total_ticks = 0; + u64 when_ticks = 0; + isize untimed = 0; + i32 largest = -1; + isize cyclic = 0; + + mutex_lock(&global_entity_time_mutex); + for (auto const &entry : global_entity_times) { + if (!entry.value.in_global_loop) { + when_ticks += entry.value.ticks; + } + } + for (i32 gi = 0; gi < group_count; gi++) { + GlobalGroup const &group = g->groups[gi]; + ticks[gi] = 0; + for (i32 k = 0; k < group.count; k++) { + Entity *e = g->nodes[g->members[group.start + k]]; + GlobalEntityTime *t = map_get(&global_entity_times, e); + untimed += t == nullptr; + u64 et = t ? t->ticks : 0; + ticks[gi] += et; + + i32 *p = map_get(&pkg_index, e->pkg); + if (p == nullptr) { + map_set(&pkg_index, e->pkg, cast(i32)pkgs.count); + array_add(&pkgs, e->pkg); + array_add(&pkg_ticks, et); + array_add(&pkg_entities, 1); + } else { + pkg_ticks[*p] += et; + pkg_entities[*p] += 1; + } + } + total_ticks += ticks[gi]; + if (largest < 0 || group.count > g->groups[largest].count) { + largest = gi; + } + cyclic += group.count > 1; + } + mutex_unlock(&global_entity_time_mutex); + + // NOTE: every dependency of a group has a lower index + i32 critical = -1; + for (i32 gi = 0; gi < group_count; gi++) { + seen[gi] = -1; + } + for (i32 gi = 0; gi < group_count; gi++) { + GlobalGroup const &group = g->groups[gi]; + prev[gi] = -1; + for (i32 k = 0; k < group.count; k++) { + i32 v = g->members[group.start + k]; + for (i32 i = g->offsets[v]; i < g->offsets[v+1]; i++) { + i32 dep = g->group_of[g->targets[i]]; + if (dep != gi && seen[dep] != gi) { + seen[dep] = gi; + if (prev[gi] < 0 || path[dep] > path[prev[gi]]) { + prev[gi] = dep; + } + } + } + } + path[gi] = ticks[gi] + (prev[gi] >= 0 ? path[prev[gi]] : 0); + len[gi] = 1 + (prev[gi] >= 0 ? len[prev[gi]] : 0); + if (critical < 0 || path[gi] > path[critical]) { + critical = gi; + } + } + + f64 total_ms = global_graph_ms(total_ticks, freq); + f64 critical_ms = critical >= 0 ? global_graph_ms(path[critical], freq) : 0; + + gb_printf_err("Global entity groups\n"); + gb_printf_err(" entities: %td (%td not timed), dependency edges: %td, missing edges: %td\n", g->nodes.count, untimed, g->targets.count, g->missing_edges); + gb_printf_err(" self time: %.3f ms in the groups, %.3f ms during 'when' resolution\n", total_ms, global_graph_ms(when_ticks, freq)); + gb_printf_err(" groups: %d (%td with a cycle), largest has %d entities\n", group_count, cyclic, largest >= 0 ? g->groups[largest].count : 0); + gb_printf_err(" critical path: %.3f ms over %d groups -> at most %.2fx speedup\n", + critical_ms, critical >= 0 ? len[critical] : 0, critical_ms > 0 ? total_ms/critical_ms : 0.0); + + gb_printf_err(" check_import_entities (sequential, includes entity checks it triggers):\n"); + for (isize i = 0; i < GlobalImportStage_COUNT; i++) { + gb_printf_err(" %10.3f ms %s\n", global_graph_ms(global_import_stage_ticks[i], freq), global_import_stage_names[i]); + } + + { + i32 const BUCKET_COUNT = 10; + i32 const bucket_max [BUCKET_COUNT] = {1, 2, 4, 8, 16, 64, 256, 1024, 4096, 0x7fffffff}; + char const *bucket_name [BUCKET_COUNT] = {"1", "2", "3-4", "5-8", "9-16", "17-64", "65-256", "257-1024", "1025-4096", ">4096"}; + isize bucket_groups [BUCKET_COUNT] = {}; + isize bucket_entities[BUCKET_COUNT] = {}; + u64 bucket_ticks [BUCKET_COUNT] = {}; + for (i32 gi = 0; gi < group_count; gi++) { + i32 count = g->groups[gi].count; + for (i32 b = 0; b < BUCKET_COUNT; b++) { + if (count <= bucket_max[b]) { + bucket_groups[b] += 1; + bucket_entities[b] += count; + bucket_ticks[b] += ticks[gi]; + break; + } + } + } + gb_printf_err(" group sizes:\n"); + gb_printf_err(" %10s %9s %10s %12s\n", "size", "groups", "entities", "self time"); + for (i32 b = 0; b < BUCKET_COUNT; b++) { + if (bucket_groups[b] != 0) { + gb_printf_err(" %10s %9td %10td %9.3f ms\n", bucket_name[b], bucket_groups[b], bucket_entities[b], global_graph_ms(bucket_ticks[b], freq)); + } + } + } + + auto items = array_make(heap_allocator(), 0, gb_max(group_count, cast(i32)pkgs.count)); + defer (array_free(&items)); + isize const TOP = 10; + + for (i32 gi = 0; gi < group_count; gi++) { + array_add(&items, GlobalGraphSortItem{ticks[gi], gi}); + } + array_sort(items, global_graph_sort_item_desc); + gb_printf_err(" slowest groups:\n"); + for (isize i = 0; i < items.count && i < TOP; i++) { + print_global_group(g, items[i].id, items[i].key, freq, 4); + } + + array_clear(&items); + for (i32 gi = 0; gi < group_count; gi++) { + if (g->groups[gi].count > 1) { + array_add(&items, GlobalGraphSortItem{cast(u64)g->groups[gi].count, gi}); + } + } + array_sort(items, global_graph_sort_item_desc); + gb_printf_err(" largest groups:\n"); + for (isize i = 0; i < items.count && i < TOP; i++) { + print_global_group(g, items[i].id, ticks[items[i].id], freq, 4); + } + + gb_printf_err(" critical path, last group first:\n"); + isize printed = 0; + for (i32 gi = critical; gi >= 0; gi = prev[gi]) { + if (printed == 30) { + gb_printf_err(" ... %d more groups\n", len[gi]); + break; + } + print_global_group(g, gi, ticks[gi], freq, 2); + printed += 1; + } + + array_clear(&items); + for (i32 p = 0; p < pkgs.count; p++) { + array_add(&items, GlobalGraphSortItem{pkg_ticks[p], p}); + } + array_sort(items, global_graph_sort_item_desc); + gb_printf_err(" slowest packages:\n"); + for (isize i = 0; i < items.count && i < TOP; i++) { + i32 p = items[i].id; + String name = pkgs[p] ? pkgs[p]->name : str_lit("?"); + gb_printf_err(" %10.3f ms %7d entities %.*s\n", global_graph_ms(pkg_ticks[p], freq), pkg_entities[p], LIT(name)); + } +} diff --git a/src/checker_global_graph.cpp b/src/checker_global_graph.cpp deleted file mode 100644 index 67a2c7c07..000000000 --- a/src/checker_global_graph.cpp +++ /dev/null @@ -1,539 +0,0 @@ -// -internal-global-entity-graph: per-entity self time of global declaration checking, -// plus statistics of the dependency graph between global entities (DeclInfo::deps) - -struct GlobalEntityTime { - u64 ticks; - bool in_global_loop; -}; - -struct GlobalEntityTimingFrame { - u64 start; - u64 saved_child_ticks; - bool active; -}; - -enum GlobalImportStagePart { - GlobalImportStage_Imports, - GlobalImportStage_Placeholders, - GlobalImportStage_DeclSources, - GlobalImportStage_TypeAliases, - GlobalImportStage_DelayedExprs, - - GlobalImportStage_COUNT, -}; - -gb_global char const *global_import_stage_names[GlobalImportStage_COUNT] = { - "imports", - "'when' and 'foreign' placeholders", - "resolve 'when' and 'foreign' blocks", - "type alias correction", - "delayed expressions (#assert etc.)", -}; - -gb_global u64 global_import_stage_ticks[GlobalImportStage_COUNT]; - -gb_internal u64 global_import_stage_begin(void) { - return build_context.internal_global_entity_graph ? time_stamp_time_now() : 0; -} - -gb_internal void global_import_stage_end(GlobalImportStagePart part, u64 start) { - if (build_context.internal_global_entity_graph) { - global_import_stage_ticks[part] += time_stamp_time_now() - start; - } -} - -gb_global BlockingMutex global_entity_time_mutex; -gb_global PtrMap global_entity_times; -gb_thread_local u64 global_entity_child_ticks; - -gb_internal GlobalEntityTimingFrame global_entity_timing_begin(Entity *e) { - GlobalEntityTimingFrame f = {}; - if (!build_context.internal_global_entity_graph) { - return f; - } - if (e->scope == nullptr || (e->scope->flags & ScopeFlag_File) == 0) { - return f; - } - f.active = true; - f.saved_child_ticks = global_entity_child_ticks; - global_entity_child_ticks = 0; - f.start = time_stamp_time_now(); - return f; -} - -gb_internal void global_entity_timing_end(GlobalEntityTimingFrame const &f, Entity *e) { - if (!f.active) { - return; - } - u64 total = time_stamp_time_now() - f.start; - u64 self = total - gb_min(total, global_entity_child_ticks); - global_entity_child_ticks = f.saved_child_ticks + total; - - bool in_global_loop = in_single_threaded_checker_stage.load(std::memory_order_relaxed); - - MUTEX_GUARD(&global_entity_time_mutex); - GlobalEntityTime *found = map_get(&global_entity_times, e); - if (found) { - found->ticks += self; - } else { - map_set(&global_entity_times, e, GlobalEntityTime{self, in_global_loop}); - } -} - - -// Iterative Tarjan; components are numbered so that every edge v->w has comp(w) <= comp(v) -gb_internal i32 global_graph_scc(i32 node_count, Array const &offsets, Array const &targets, Array *comp_of_) { - struct Frame { - i32 v; - i32 pos; - }; - - auto index = array_make(heap_allocator(), node_count); - auto low = array_make(heap_allocator(), node_count); - auto on_stack = array_make(heap_allocator(), node_count); - auto stack = array_make(heap_allocator(), 0, node_count); - auto frames = array_make(heap_allocator(), 0, 64); - defer (array_free(&index)); - defer (array_free(&low)); - defer (array_free(&on_stack)); - defer (array_free(&stack)); - defer (array_free(&frames)); - - Array &comp_of = *comp_of_; - for (i32 v = 0; v < node_count; v++) { - index[v] = -1; - low[v] = 0; - on_stack[v] = false; - comp_of[v] = -1; - } - - i32 counter = 0; - i32 comp_count = 0; - for (i32 root = 0; root < node_count; root++) { - if (index[root] >= 0) { - continue; - } - index[root] = low[root] = counter++; - array_add(&stack, root); - on_stack[root] = true; - array_add(&frames, Frame{root, 0}); - - while (frames.count > 0) { - Frame *top = &frames[frames.count-1]; - i32 v = top->v; - if (offsets[v] + top->pos < offsets[v+1]) { - i32 w = targets[offsets[v] + top->pos]; - top->pos += 1; - if (index[w] < 0) { - index[w] = low[w] = counter++; - array_add(&stack, w); - on_stack[w] = true; - array_add(&frames, Frame{w, 0}); - } else if (on_stack[w]) { - low[v] = gb_min(low[v], index[w]); - } - continue; - } - - if (low[v] == index[v]) { - for (;;) { - i32 w = array_pop(&stack); - on_stack[w] = false; - comp_of[w] = comp_count; - if (w == v) { - break; - } - } - comp_count += 1; - } - array_pop(&frames); - if (frames.count > 0) { - i32 u = frames[frames.count-1].v; - low[u] = gb_min(low[u], low[v]); - } - } - } - return comp_count; -} - -struct GlobalGraphComp { - i32 start; // into the member order array - i32 count; - u64 ticks; - u64 path_ticks; // heaviest dependency chain ending at this component, inclusive - i32 path_len; - i32 path_prev; - bool has_self_edge; -}; - -struct GlobalGraphSortItem { - u64 key; - i32 id; -}; - -gb_internal GB_COMPARE_PROC(global_graph_sort_item_desc) { - GlobalGraphSortItem const *x = cast(GlobalGraphSortItem const *)a; - GlobalGraphSortItem const *y = cast(GlobalGraphSortItem const *)b; - if (x->key != y->key) { - return x->key > y->key ? -1 : +1; - } - return i32_cmp(x->id, y->id); -} - -// Fills `comps` (indexed by component) and `order` (node indices grouped by component) -gb_internal void global_graph_condense(i32 node_count, Array const &offsets, Array const &targets, Array const &node_ticks, - Array const &comp_of, i32 comp_count, Array *comps_, Array *order_) { - Array &comps = *comps_; - Array &order = *order_; - for (i32 i = 0; i < comp_count; i++) { - comps[i] = {}; - comps[i].path_prev = -1; - } - for (i32 v = 0; v < node_count; v++) { - GlobalGraphComp *g = &comps[comp_of[v]]; - g->count += 1; - g->ticks += node_ticks[v]; - } - i32 start = 0; - for (i32 i = 0; i < comp_count; i++) { - comps[i].start = start; - start += comps[i].count; - comps[i].count = 0; - } - for (i32 v = 0; v < node_count; v++) { - GlobalGraphComp *g = &comps[comp_of[v]]; - order[g->start + g->count] = v; - g->count += 1; - } - - for (i32 ci = 0; ci < comp_count; ci++) { - GlobalGraphComp *g = &comps[ci]; - u64 best_ticks = 0; - i32 best_len = 0; - i32 best_prev = -1; - for (i32 k = 0; k < g->count; k++) { - i32 v = order[g->start + k]; - for (i32 e = offsets[v]; e < offsets[v+1]; e++) { - i32 w = targets[e]; - if (w == v) { - g->has_self_edge = true; - } - i32 cw = comp_of[w]; - if (cw == ci) { - continue; - } - GB_ASSERT(cw < ci); - GlobalGraphComp *d = &comps[cw]; - if (best_prev < 0 || d->path_ticks > best_ticks) { - best_ticks = d->path_ticks; - best_prev = cw; - } - best_len = gb_max(best_len, d->path_len); - } - } - g->path_ticks = g->ticks + best_ticks; - g->path_len = 1 + best_len; - g->path_prev = best_prev; - } -} - -gb_internal f64 global_graph_ms(u64 ticks, u64 freq) { - return 1000.0 * cast(f64)ticks / cast(f64)freq; -} - -gb_internal void global_graph_print_entity(Entity *e) { - String pkg = e->pkg ? e->pkg->name : str_lit("?"); - String file = e->file ? filename_without_directory(e->file->fullpath) : str_lit("?"); - gb_printf_err("%.*s.%.*s (%.*s:%d)", LIT(pkg), LIT(e->token.string), LIT(file), e->token.pos.line); -} - -gb_internal void global_graph_print_comp(Array const &nodes, Array const &order, GlobalGraphComp const *g, u64 ticks, u64 freq, isize max_names) { - gb_printf_err(" %10.3f ms %7d entities ", global_graph_ms(ticks, freq), g->count); - for (i32 k = 0; k < g->count && k < max_names; k++) { - if (k > 0) { - gb_printf_err(", "); - } - global_graph_print_entity(nodes[order[g->start + k]]); - } - if (g->count > max_names) { - gb_printf_err(", ..."); - } - gb_printf_err("\n"); -} - -gb_internal void print_global_entity_graph(Checker *c) { - u64 const freq = time_stamp__freq(); - - // Nodes - auto nodes = array_make(heap_allocator(), 0, c->info.entities.count); - defer (array_free(&nodes)); - PtrMap node_index = {}; - map_init(&node_index, c->info.entities.count); - defer (map_destroy(&node_index)); - - for (Entity *e : c->info.entities) { - if (e->decl_info == nullptr || e->scope == nullptr || (e->scope->flags & ScopeFlag_File) == 0) { - continue; - } - if (map_get(&node_index, e) != nullptr) { - continue; - } - map_set(&node_index, e, cast(i32)nodes.count); - array_add(&nodes, e); - } - i32 node_count = cast(i32)nodes.count; - - // Edges, from what each declaration actually used while being checked - auto offsets = array_make(heap_allocator(), node_count+1); - auto targets = array_make(heap_allocator(), 0, node_count*4); - defer (array_free(&offsets)); - defer (array_free(&targets)); - isize self_edges = 0; - for (i32 v = 0; v < node_count; v++) { - offsets[v] = cast(i32)targets.count; - DeclInfo *d = nodes[v]->decl_info; - FOR_PTR_SET(dep, d->deps) { - i32 *w = map_get(&node_index, dep); - if (w != nullptr) { - array_add(&targets, *w); - self_edges += *w == v; - } - } - } - offsets[node_count] = cast(i32)targets.count; - - // Weights - auto node_ticks = array_make(heap_allocator(), node_count); - defer (array_free(&node_ticks)); - u64 total_ticks = 0; - u64 when_ticks = 0; - isize untimed = 0; - mutex_lock(&global_entity_time_mutex); - for (i32 v = 0; v < node_count; v++) { - GlobalEntityTime *t = map_get(&global_entity_times, nodes[v]); - node_ticks[v] = t ? t->ticks : 0; - total_ticks += node_ticks[v]; - if (t == nullptr) { - untimed += 1; - } else if (!t->in_global_loop) { - when_ticks += t->ticks; - } - } - mutex_unlock(&global_entity_time_mutex); - - // Entity groups - auto comp_of = array_make(heap_allocator(), node_count); - defer (array_free(&comp_of)); - i32 comp_count = global_graph_scc(node_count, offsets, targets, &comp_of); - - auto comps = array_make(heap_allocator(), comp_count); - auto order = array_make(heap_allocator(), node_count); - defer (array_free(&comps)); - defer (array_free(&order)); - global_graph_condense(node_count, offsets, targets, node_ticks, comp_of, comp_count, &comps, &order); - - i32 critical = -1; - i32 largest = -1; - isize cyclic_comps = 0; - for (i32 ci = 0; ci < comp_count; ci++) { - GlobalGraphComp *g = &comps[ci]; - if (critical < 0 || g->path_ticks > comps[critical].path_ticks) { - critical = ci; - } - if (largest < 0 || g->count > comps[largest].count) { - largest = ci; - } - cyclic_comps += g->count > 1 || g->has_self_edge; - } - - // Packages, weighted by the entities they own - PtrMap pkg_index = {}; - map_init(&pkg_index, c->info.packages.count); - defer (map_destroy(&pkg_index)); - auto pkgs = array_make(heap_allocator(), 0, c->info.packages.count); - defer (array_free(&pkgs)); - auto node_pkg = array_make(heap_allocator(), node_count); - defer (array_free(&node_pkg)); - for (i32 v = 0; v < node_count; v++) { - AstPackage *p = nodes[v]->pkg; - i32 *found = map_get(&pkg_index, p); - if (found == nullptr) { - map_set(&pkg_index, p, cast(i32)pkgs.count); - node_pkg[v] = cast(i32)pkgs.count; - array_add(&pkgs, p); - } else { - node_pkg[v] = *found; - } - } - i32 pkg_count = cast(i32)pkgs.count; - - auto pkg_ticks = array_make(heap_allocator(), pkg_count); - auto pkg_entities = array_make(heap_allocator(), pkg_count); - defer (array_free(&pkg_ticks)); - defer (array_free(&pkg_entities)); - for (i32 p = 0; p < pkg_count; p++) { - pkg_ticks[p] = 0; - pkg_entities[p] = 0; - } - for (i32 v = 0; v < node_count; v++) { - pkg_ticks[node_pkg[v]] += node_ticks[v]; - pkg_entities[node_pkg[v]] += 1; - } - - auto pkg_offsets = array_make(heap_allocator(), pkg_count+1); - auto pkg_targets = array_make(heap_allocator(), 0, pkg_count*4); - defer (array_free(&pkg_offsets)); - defer (array_free(&pkg_targets)); - { - auto pkg_edges = array_make>(heap_allocator(), pkg_count); - PtrMap seen = {}; - map_init(&seen); - for (i32 p = 0; p < pkg_count; p++) { - pkg_edges[p] = array_make(heap_allocator()); - } - for (i32 v = 0; v < node_count; v++) { - for (i32 e = offsets[v]; e < offsets[v+1]; e++) { - i32 pv = node_pkg[v]; - i32 pw = node_pkg[targets[e]]; - if (pv == pw) { - continue; - } - u64 key = (cast(u64)(pv+1) << 32) | cast(u64)(pw+1); - if (map_get(&seen, key) == nullptr) { - map_set(&seen, key, true); - array_add(&pkg_edges[pv], pw); - } - } - } - for (i32 p = 0; p < pkg_count; p++) { - pkg_offsets[p] = cast(i32)pkg_targets.count; - for (i32 w : pkg_edges[p]) { - array_add(&pkg_targets, w); - } - array_free(&pkg_edges[p]); - } - pkg_offsets[pkg_count] = cast(i32)pkg_targets.count; - array_free(&pkg_edges); - map_destroy(&seen); - } - - auto pkg_comp_of = array_make(heap_allocator(), pkg_count); - defer (array_free(&pkg_comp_of)); - i32 pkg_comp_count = global_graph_scc(pkg_count, pkg_offsets, pkg_targets, &pkg_comp_of); - auto pkg_comps = array_make(heap_allocator(), pkg_comp_count); - auto pkg_order = array_make(heap_allocator(), pkg_count); - defer (array_free(&pkg_comps)); - defer (array_free(&pkg_order)); - global_graph_condense(pkg_count, pkg_offsets, pkg_targets, pkg_ticks, pkg_comp_of, pkg_comp_count, &pkg_comps, &pkg_order); - u64 pkg_critical_ticks = 0; - isize pkg_cyclic = 0; - for (i32 ci = 0; ci < pkg_comp_count; ci++) { - pkg_critical_ticks = gb_max(pkg_critical_ticks, pkg_comps[ci].path_ticks); - pkg_cyclic += pkg_comps[ci].count > 1; - } - - // Report - f64 total_ms = global_graph_ms(total_ticks, freq); - f64 critical_ms = critical >= 0 ? global_graph_ms(comps[critical].path_ticks, freq) : 0; - f64 pkg_critical_ms = global_graph_ms(pkg_critical_ticks, freq); - - gb_printf_err("Global entity graph\n"); - gb_printf_err(" entities: %td (%td not timed), dependency edges: %td (%td self edges)\n", nodes.count, untimed, targets.count, self_edges); - gb_printf_err(" self time: %.3f ms total; %.3f ms during 'when' resolution, %.3f ms in the global loop\n", - total_ms, global_graph_ms(when_ticks, freq), global_graph_ms(total_ticks - when_ticks, freq)); - gb_printf_err(" groups: %d (%td with a cycle), largest has %d entities\n", - comp_count, cyclic_comps, largest >= 0 ? comps[largest].count : 0); - gb_printf_err(" critical path (entity groups): %.3f ms over %d groups -> at most %.2fx speedup\n", - critical_ms, critical >= 0 ? comps[critical].path_len : 0, critical_ms > 0 ? total_ms/critical_ms : 0.0); - gb_printf_err(" critical path (packages): %.3f ms -> at most %.2fx speedup (%d packages, %td package cycles)\n", - pkg_critical_ms, pkg_critical_ms > 0 ? total_ms/pkg_critical_ms : 0.0, pkg_count, pkg_cyclic); - - gb_printf_err(" check_import_entities (sequential, includes entity checks it triggers):\n"); - for (isize i = 0; i < GlobalImportStage_COUNT; i++) { - gb_printf_err(" %10.3f ms %s\n", global_graph_ms(global_import_stage_ticks[i], freq), global_import_stage_names[i]); - } - - { - i32 const BUCKET_COUNT = 10; - i32 const bucket_max[BUCKET_COUNT] = {1, 2, 4, 8, 16, 64, 256, 1024, 4096, 0x7fffffff}; - char const *bucket_name[BUCKET_COUNT] = {"1", "2", "3-4", "5-8", "9-16", "17-64", "65-256", "257-1024", "1025-4096", ">4096"}; - isize bucket_groups[BUCKET_COUNT] = {}; - isize bucket_entities[BUCKET_COUNT] = {}; - u64 bucket_ticks[BUCKET_COUNT] = {}; - for (i32 ci = 0; ci < comp_count; ci++) { - GlobalGraphComp *g = &comps[ci]; - for (i32 b = 0; b < BUCKET_COUNT; b++) { - if (g->count <= bucket_max[b]) { - bucket_groups[b] += 1; - bucket_entities[b] += g->count; - bucket_ticks[b] += g->ticks; - break; - } - } - } - gb_printf_err(" group sizes:\n"); - gb_printf_err(" %10s %9s %10s %12s\n", "size", "groups", "entities", "self time"); - for (i32 b = 0; b < BUCKET_COUNT; b++) { - if (bucket_groups[b] == 0) { - continue; - } - gb_printf_err(" %10s %9td %10td %9.3f ms\n", bucket_name[b], bucket_groups[b], bucket_entities[b], global_graph_ms(bucket_ticks[b], freq)); - } - } - - auto items = array_make(heap_allocator(), 0, gb_max(comp_count, pkg_count)); - defer (array_free(&items)); - - isize const TOP = 10; - isize const MAX_NAMES = 4; - - array_clear(&items); - for (i32 ci = 0; ci < comp_count; ci++) { - array_add(&items, GlobalGraphSortItem{comps[ci].ticks, ci}); - } - array_sort(items, global_graph_sort_item_desc); - gb_printf_err(" slowest groups:\n"); - for (isize i = 0; i < items.count && i < TOP; i++) { - GlobalGraphComp *g = &comps[items[i].id]; - global_graph_print_comp(nodes, order, g, g->ticks, freq, MAX_NAMES); - } - - array_clear(&items); - for (i32 ci = 0; ci < comp_count; ci++) { - if (comps[ci].count > 1) { - array_add(&items, GlobalGraphSortItem{cast(u64)comps[ci].count, ci}); - } - } - array_sort(items, global_graph_sort_item_desc); - gb_printf_err(" largest groups:\n"); - for (isize i = 0; i < items.count && i < TOP; i++) { - GlobalGraphComp *g = &comps[items[i].id]; - global_graph_print_comp(nodes, order, g, g->ticks, freq, MAX_NAMES); - } - - gb_printf_err(" critical path, last group first:\n"); - { - isize printed = 0; - for (i32 ci = critical; ci >= 0; ci = comps[ci].path_prev) { - if (printed == 30) { - gb_printf_err(" ... %d more groups\n", comps[ci].path_len); - break; - } - global_graph_print_comp(nodes, order, &comps[ci], comps[ci].ticks, freq, 2); - printed += 1; - } - } - - array_clear(&items); - for (i32 p = 0; p < pkg_count; p++) { - array_add(&items, GlobalGraphSortItem{pkg_ticks[p], p}); - } - array_sort(items, global_graph_sort_item_desc); - gb_printf_err(" slowest packages:\n"); - for (isize i = 0; i < items.count && i < TOP; i++) { - i32 p = items[i].id; - AstPackage *pkg = pkgs[p]; - String name = pkg ? pkg->name : str_lit("?"); - gb_printf_err(" %10.3f ms %7d entities %.*s\n", global_graph_ms(pkg_ticks[p], freq), pkg_entities[p], LIT(name)); - } -} diff --git a/src/checker_global_groups.cpp b/src/checker_global_groups.cpp deleted file mode 100644 index cf3e3924e..000000000 --- a/src/checker_global_groups.cpp +++ /dev/null @@ -1,769 +0,0 @@ -// Global entities are checked in groups: the strongly connected components of a dependency graph built -// from syntax, in dependency order. A group only names entities of its own or of finished groups, which -// `-internal-check-global-edges` verifies - -struct GlobalGroup { - i32 start; // into `GlobalGroupGraph::members` - i32 count; - bool done; -}; - -struct GlobalGroupGraph { - Array nodes; - PtrMap node_of; - Array offsets; // node -> the nodes it names, as `targets[offsets[v]..offsets[v+1]]` - Array targets; - Array group_of; - Array groups; // every dependency of a group has a lower index - Array members; // nodes, by group, in source order - - bool active; - i32 current_group; - Entity *current_entity; - isize missing_edges; -}; - -gb_global GlobalGroupGraph global_groups; - -struct GlobalGraphWalk { - Scope *scope; - Array *refs; -}; - -gb_internal void global_graph_walk(GlobalGraphWalk *w, Ast *node); - -gb_internal void global_graph_walk_slice(GlobalGraphWalk *w, Slice const &nodes) { - for (Ast *node : nodes) { - global_graph_walk(w, node); - } -} - -gb_internal void global_graph_add_ref(GlobalGraphWalk *w, Entity *e) { - if (e != nullptr) { - array_add(w->refs, e); - } -} - -// NOTE: names bound within the expression (parameters, fields, '$T') are also looked up globally, which at -// worst adds an edge; only procedure bodies are skipped, as they are checked after this stage -gb_internal void global_graph_walk(GlobalGraphWalk *w, Ast *node) { - if (node == nullptr) { - return; - } - switch (node->kind) { - case Ast_Ident: - global_graph_add_ref(w, scope_lookup(w->scope, node->Ident.interned, node->Ident.hash)); - break; - - case Ast_SelectorExpr: { - Ast *expr = node->SelectorExpr.expr; - Ast *selector = node->SelectorExpr.selector; - if (expr != nullptr && expr->kind == Ast_Ident) { - Entity *e = scope_lookup(w->scope, expr->Ident.interned, expr->Ident.hash); - if (e != nullptr && e->kind == Entity_ImportName && selector != nullptr && selector->kind == Ast_Ident) { - global_graph_add_ref(w, scope_lookup_current(e->ImportName.scope, selector->Ident.interned, selector->Ident.hash)); - } else { - global_graph_add_ref(w, e); - } - } else { - global_graph_walk(w, expr); - } - } break; - - case Ast_PolyType: - global_graph_walk(w, node->PolyType.specialization); - break; - case Ast_Ellipsis: - global_graph_walk(w, node->Ellipsis.expr); - break; - case Ast_ProcGroup: - global_graph_walk_slice(w, node->ProcGroup.args); - break; - case Ast_AsmGroup: - global_graph_walk_slice(w, node->AsmGroup.args); - break; - case Ast_ProcLit: - global_graph_walk(w, node->ProcLit.type); - global_graph_walk_slice(w, node->ProcLit.where_clauses); - break; - case Ast_CompoundLit: - global_graph_walk(w, node->CompoundLit.type); - global_graph_walk_slice(w, node->CompoundLit.elems); - global_graph_walk(w, node->CompoundLit.tag); - break; - case Ast_TagExpr: - global_graph_walk(w, node->TagExpr.expr); - break; - case Ast_UnaryExpr: - global_graph_walk(w, node->UnaryExpr.expr); - break; - case Ast_BinaryExpr: - global_graph_walk(w, node->BinaryExpr.left); - global_graph_walk(w, node->BinaryExpr.right); - break; - case Ast_ParenExpr: - global_graph_walk(w, node->ParenExpr.expr); - break; - case Ast_SelectorCallExpr: - global_graph_walk(w, node->SelectorCallExpr.expr); - global_graph_walk(w, node->SelectorCallExpr.call); - break; - case Ast_IndexExpr: - global_graph_walk(w, node->IndexExpr.expr); - global_graph_walk(w, node->IndexExpr.index); - break; - case Ast_MatrixIndexExpr: - global_graph_walk(w, node->MatrixIndexExpr.expr); - global_graph_walk(w, node->MatrixIndexExpr.row_index); - global_graph_walk(w, node->MatrixIndexExpr.column_index); - break; - case Ast_DerefExpr: - global_graph_walk(w, node->DerefExpr.expr); - break; - case Ast_SliceExpr: - global_graph_walk(w, node->SliceExpr.expr); - global_graph_walk(w, node->SliceExpr.low); - global_graph_walk(w, node->SliceExpr.high); - break; - case Ast_CallExpr: - global_graph_walk(w, node->CallExpr.proc); - global_graph_walk_slice(w, node->CallExpr.args); - break; - case Ast_FieldValue: - global_graph_walk(w, node->FieldValue.field); - global_graph_walk(w, node->FieldValue.value); - break; - case Ast_EnumFieldValue: - global_graph_walk(w, node->EnumFieldValue.value); - break; - case Ast_TernaryIfExpr: - global_graph_walk(w, node->TernaryIfExpr.x); - global_graph_walk(w, node->TernaryIfExpr.cond); - global_graph_walk(w, node->TernaryIfExpr.y); - break; - case Ast_TernaryWhenExpr: - global_graph_walk(w, node->TernaryWhenExpr.x); - global_graph_walk(w, node->TernaryWhenExpr.cond); - global_graph_walk(w, node->TernaryWhenExpr.y); - break; - case Ast_OrElseExpr: - global_graph_walk(w, node->OrElseExpr.x); - global_graph_walk(w, node->OrElseExpr.y); - break; - case Ast_OrReturnExpr: - global_graph_walk(w, node->OrReturnExpr.expr); - break; - case Ast_OrBranchExpr: - global_graph_walk(w, node->OrBranchExpr.expr); - break; - case Ast_TypeAssertion: - global_graph_walk(w, node->TypeAssertion.expr); - global_graph_walk(w, node->TypeAssertion.type); - break; - case Ast_TypeCast: - global_graph_walk(w, node->TypeCast.type); - global_graph_walk(w, node->TypeCast.expr); - break; - case Ast_AutoCast: - global_graph_walk(w, node->AutoCast.expr); - break; - - case Ast_Field: - global_graph_walk(w, node->Field.type); - global_graph_walk(w, node->Field.default_value); - break; - case Ast_BitFieldField: - global_graph_walk(w, node->BitFieldField.type); - global_graph_walk(w, node->BitFieldField.bit_size); - break; - case Ast_FieldList: - global_graph_walk_slice(w, node->FieldList.list); - break; - - case Ast_TypeidType: - global_graph_walk(w, node->TypeidType.specialization); - break; - case Ast_HelperType: - global_graph_walk(w, node->HelperType.type); - break; - case Ast_DistinctType: - global_graph_walk(w, node->DistinctType.type); - break; - case Ast_ProcType: - global_graph_walk(w, node->ProcType.params); - global_graph_walk(w, node->ProcType.results); - break; - case Ast_RelativeType: - global_graph_walk(w, node->RelativeType.tag); - global_graph_walk(w, node->RelativeType.type); - break; - case Ast_PointerType: - global_graph_walk(w, node->PointerType.type); - global_graph_walk(w, node->PointerType.tag); - break; - case Ast_MultiPointerType: - global_graph_walk(w, node->MultiPointerType.type); - break; - case Ast_ArrayType: - global_graph_walk(w, node->ArrayType.count); - global_graph_walk(w, node->ArrayType.elem); - global_graph_walk(w, node->ArrayType.tag); - break; - case Ast_DynamicArrayType: - global_graph_walk(w, node->DynamicArrayType.elem); - global_graph_walk(w, node->DynamicArrayType.tag); - break; - case Ast_FixedCapacityDynamicArrayType: - global_graph_walk(w, node->FixedCapacityDynamicArrayType.elem); - global_graph_walk(w, node->FixedCapacityDynamicArrayType.capacity); - global_graph_walk(w, node->FixedCapacityDynamicArrayType.tag); - break; - case Ast_StructType: - global_graph_walk_slice(w, node->StructType.fields); - global_graph_walk(w, node->StructType.polymorphic_params); - global_graph_walk(w, node->StructType.align); - global_graph_walk(w, node->StructType.min_field_align); - global_graph_walk(w, node->StructType.max_field_align); - global_graph_walk_slice(w, node->StructType.where_clauses); - break; - case Ast_UnionType: - global_graph_walk_slice(w, node->UnionType.variants); - global_graph_walk(w, node->UnionType.polymorphic_params); - global_graph_walk_slice(w, node->UnionType.where_clauses); - break; - case Ast_EnumType: - global_graph_walk(w, node->EnumType.base_type); - for (Ast *field : node->EnumType.fields) { - if (field->kind == Ast_EnumFieldValue) { - global_graph_walk(w, field->EnumFieldValue.value); - } - } - break; - case Ast_BitSetType: - global_graph_walk(w, node->BitSetType.elem); - global_graph_walk(w, node->BitSetType.underlying); - break; - case Ast_BitFieldType: - global_graph_walk(w, node->BitFieldType.backing_type); - global_graph_walk_slice(w, node->BitFieldType.fields); - break; - case Ast_MapType: - global_graph_walk(w, node->MapType.count); - global_graph_walk(w, node->MapType.key); - global_graph_walk(w, node->MapType.value); - break; - case Ast_MatrixType: - global_graph_walk(w, node->MatrixType.row_count); - global_graph_walk(w, node->MatrixType.column_count); - global_graph_walk(w, node->MatrixType.elem); - break; - - case Ast_AsmTemplate: - global_graph_walk(w, node->AsmTemplate.signature); - global_graph_walk_slice(w, node->AsmTemplate.specs); - global_graph_walk_slice(w, node->AsmTemplate.clobbers); - global_graph_walk_slice(w, node->AsmTemplate.instructions); - break; - case Ast_AsmSpec: - global_graph_walk(w, node->AsmSpec.type); - global_graph_walk(w, node->AsmSpec.value); - for (Ast *d : node->AsmSpec.directives) { - global_graph_walk(w, d); - } - break; - case Ast_AsmClobber: - global_graph_walk(w, node->AsmClobber.value); - break; - case Ast_AsmInstruction: - global_graph_walk_slice(w, node->AsmInstruction.operands); - break; - case Ast_AsmMemoryTerm: - global_graph_walk(w, node->AsmMemoryTerm.operand); - global_graph_walk(w, node->AsmMemoryTerm.scale); - break; - case Ast_AsmMemoryOperand: - global_graph_walk(w, node->AsmMemoryOperand.segment_override); - global_graph_walk_slice(w, node->AsmMemoryOperand.terms); - global_graph_walk(w, node->AsmMemoryOperand.type); - break; - case Ast_AsmRegisterGroup: - for (Ast *r : node->AsmRegisterGroup.registers) { - global_graph_walk(w, r); - } - global_graph_walk(w, node->AsmRegisterGroup.type); - break; - case Ast_AsmDirective: - global_graph_walk_slice(w, node->AsmDirective.operands); - break; - } -} - -gb_internal void global_graph_walk_entity(GlobalGraphWalk *w, Entity *e, DeclInfo *d) { - w->scope = d->scope; - global_graph_walk(w, d->type_expr); - global_graph_walk(w, d->init_expr); - for (Ast *attr : d->attributes) { - if (attr->kind != Ast_Attribute) { - continue; - } - for (Ast *elem : attr->Attribute.elems) { - if (elem->kind == Ast_FieldValue) { - global_graph_walk(w, elem->FieldValue.value); - } - } - } - if (e->kind == Entity_Procedure) { - global_graph_walk(w, e->Procedure.foreign_library_ident); - } else if (e->kind == Entity_Variable) { - global_graph_walk(w, e->Variable.foreign_library_ident); - } -} - -gb_internal bool is_global_graph_node(Entity *e) { - if (e->state == EntityState_Resolved) { - return false; - } - DeclInfo *d = e->decl_info; - if (d == nullptr || e->scope == nullptr || d->scope != e->scope || (e->scope->flags & ScopeFlag_File) == 0) { - return false; - } - switch (e->kind) { - case Entity_Constant: - case Entity_TypeName: - case Entity_Variable: - case Entity_Procedure: - case Entity_ProcGroup: - case Entity_AsmTemplate: - return true; - } - return false; -} - -gb_internal i32 global_graph_add_node(GlobalGroupGraph *g, Entity *e) { - i32 *found = map_get(&g->node_of, e); - if (found != nullptr) { - return *found; - } - i32 v = cast(i32)g->nodes.count; - map_set(&g->node_of, e, v); - array_add(&g->nodes, e); - return v; -} - -gb_internal u64 global_group_random(u64 *state) { - *state = *state*6364136223846793005ull + 1442695040888963407ull; - return *state >> 33; -} - -// The nodes `[lo, hi)` walked on one thread; a name that is not a node yet is kept as an entity in `refs` -struct GlobalGraphWalkChunk { - GlobalGroupGraph *g; - i32 lo, hi; - Array targets; - Array target_ends; // per node - Array refs; - Array ref_ends; // per node -}; - -gb_internal WORKER_TASK_PROC(global_graph_walk_worker) { - GlobalGraphWalkChunk *chunk = cast(GlobalGraphWalkChunk *)data; - GlobalGroupGraph *g = chunk->g; - - auto refs = array_make(heap_allocator(), 0, 64); - defer (array_free(&refs)); - GlobalGraphWalk w = {}; - w.refs = &refs; - for (i32 v = chunk->lo; v < chunk->hi; v++) { - Entity *e = g->nodes[v]; - array_clear(&refs); - global_graph_walk_entity(&w, e, e->decl_info); - for (Entity *r : refs) { - i32 *found = map_get(&g->node_of, r); - if (found != nullptr) { - array_add(&chunk->targets, *found); - } else if (r->flags & EntityFlag_Lazy) { - array_add(&chunk->refs, r); - } - } - array_add(&chunk->target_ends, cast(i32)chunk->targets.count); - array_add(&chunk->ref_ends, cast(i32)chunk->refs.count); - } - return 0; -} - -gb_internal void build_global_groups(Checker *c, GlobalGroupGraph *g) { - array_init(&g->nodes, heap_allocator(), 0, c->info.entities.count); - map_init(&g->node_of, c->info.entities.count); - for (Entity *e : c->info.entities) { - if ((e->flags & EntityFlag_Lazy) == 0 && is_global_graph_node(e)) { - global_graph_add_node(g, e); - } - } - - // NOTE: walked in parallel, as nothing writes to the scopes now - i32 const CHUNK_SIZE = 64; - i32 initial_count = cast(i32)g->nodes.count; - auto chunks = array_make(heap_allocator(), (initial_count + CHUNK_SIZE-1)/CHUNK_SIZE); - defer (array_free(&chunks)); - for (isize i = 0; i < chunks.count; i++) { - GlobalGraphWalkChunk *chunk = &chunks[i]; - *chunk = {}; - chunk->g = g; - chunk->lo = cast(i32)(i*CHUNK_SIZE); - chunk->hi = gb_min(chunk->lo + CHUNK_SIZE, initial_count); - array_init(&chunk->targets, heap_allocator(), 0, 4*CHUNK_SIZE); - array_init(&chunk->target_ends, heap_allocator(), 0, CHUNK_SIZE); - array_init(&chunk->refs, heap_allocator(), 0); - array_init(&chunk->ref_ends, heap_allocator(), 0, CHUNK_SIZE); - thread_pool_add_task(global_graph_walk_worker, chunk); - } - thread_pool_wait(); - - auto edge_from = array_make(heap_allocator(), 0, 4*g->nodes.count); - auto edge_to = array_make(heap_allocator(), 0, 4*g->nodes.count); - auto refs = array_make(heap_allocator(), 0, 64); - defer (array_free(&edge_from)); - defer (array_free(&edge_to)); - defer (array_free(&refs)); - - auto add_ref = [&](i32 v, Entity *r) { - i32 *found = map_get(&g->node_of, r); - if (found != nullptr) { - array_add(&edge_from, v); - array_add(&edge_to, *found); - } else if ((r->flags & EntityFlag_Lazy) && is_global_graph_node(r)) { - // NOTE: a lazy entity becomes a node once a node names it - array_add(&edge_from, v); - array_add(&edge_to, global_graph_add_node(g, r)); - } - }; - - GlobalGraphWalk w = {}; - w.refs = &refs; - i32 first_of_decl = -1; - for (i32 v = 0; v < g->nodes.count; v++) { - Entity *e = g->nodes[v]; - DeclInfo *d = e->decl_info; - - // NOTE: entities sharing one declaration share its AST, e.g. `a, b: struct{x: int}`, so they share a group; - // in source order they are adjacent, and lazy ones are only checked under `lazy_mutex` - if (first_of_decl >= 0 && d->decl_node != nullptr && g->nodes[first_of_decl]->decl_info->decl_node == d->decl_node) { - array_add(&edge_from, v); - array_add(&edge_to, first_of_decl); - array_add(&edge_from, first_of_decl); - array_add(&edge_to, v); - } else { - first_of_decl = v; - } - - if (v < initial_count) { - GlobalGraphWalkChunk *chunk = &chunks[v / CHUNK_SIZE]; - i32 k = v - chunk->lo; - for (i32 i = k > 0 ? chunk->target_ends[k-1] : 0; i < chunk->target_ends[k]; i++) { - array_add(&edge_from, v); - array_add(&edge_to, chunk->targets[i]); - } - for (i32 i = k > 0 ? chunk->ref_ends[k-1] : 0; i < chunk->ref_ends[k]; i++) { - add_ref(v, chunk->refs[i]); - } - } else { - array_clear(&refs); - global_graph_walk_entity(&w, e, d); - for (Entity *r : refs) { - add_ref(v, r); - } - } - } - for (GlobalGraphWalkChunk &chunk : chunks) { - array_free(&chunk.targets); - array_free(&chunk.target_ends); - array_free(&chunk.refs); - array_free(&chunk.ref_ends); - } - - i32 node_count = cast(i32)g->nodes.count; - array_init(&g->offsets, heap_allocator(), node_count+1); - array_init(&g->targets, heap_allocator(), edge_to.count); - for (i32 v = 0; v <= node_count; v++) { - g->offsets[v] = 0; - } - for (i32 from : edge_from) { - g->offsets[from+1] += 1; - } - for (i32 v = 0; v < node_count; v++) { - g->offsets[v+1] += g->offsets[v]; - } - { - auto fill = array_clone(heap_allocator(), g->offsets); - defer (array_free(&fill)); - for (isize i = 0; i < edge_from.count; i++) { - g->targets[fill[edge_from[i]]++] = edge_to[i]; - } - } - - array_init(&g->group_of, heap_allocator(), node_count); - i32 group_count = global_graph_scc(node_count, g->offsets, g->targets, &g->group_of); - - array_init(&g->groups, heap_allocator(), group_count); - array_init(&g->members, heap_allocator(), node_count); - for (i32 gi = 0; gi < group_count; gi++) { - g->groups[gi] = {}; - } - for (i32 v = 0; v < node_count; v++) { - g->groups[g->group_of[v]].count += 1; - } - i32 start = 0; - for (i32 gi = 0; gi < group_count; gi++) { - g->groups[gi].start = start; - start += g->groups[gi].count; - g->groups[gi].count = 0; - } - // NOTE: in source order, as `c->info.entities` is sorted; lazy nodes come last, but are not checked here - for (i32 v = 0; v < node_count; v++) { - GlobalGroup *group = &g->groups[g->group_of[v]]; - g->members[group->start + group->count++] = v; - } -} - -gb_internal void global_group_print_entity(Entity *e) { - if (e == nullptr) { - gb_printf_err("?"); - return; - } - global_graph_print_entity(e); -} - -// Called when `e` starts being checked: it must be in the current group or a finished one -gb_internal void global_group_check_edge(CheckerContext *ctx, Entity *e) { - GlobalGroupGraph *g = &global_groups; - if (!g->active) { - return; - } - i32 *v = map_get(&g->node_of, e); - if (v == nullptr) { - if (!is_global_graph_node(e)) { - return; - } - } else { - i32 gi = g->group_of[*v]; - if (gi == g->current_group || g->groups[gi].done) { - return; - } - } - - g->missing_edges += 1; - if (build_context.internal_check_global_edges) { - Entity *by = ctx->decl ? ctx->decl->entity.load() : nullptr; - gb_printf_err("Missing global dependency: "); - global_group_print_entity(by); - gb_printf_err(" needs "); - global_group_print_entity(e); - gb_printf_err(v == nullptr ? ", which is not in the graph" : ""); - if (g->current_entity != by) { - gb_printf_err(", while checking "); - global_group_print_entity(g->current_entity); - } - gb_printf_err("\n"); - } -} - -// NOTE: members in a fixed order, as which member of a cycle is entered first can decide whether it checks, -// e.g. an enum whose values are `union_variant_index`es of a union with pointers back to it -gb_internal void check_global_group(Checker *c, GlobalGroupGraph *g, i32 gi) { - GlobalGroup *group = &g->groups[gi]; - i32 *members = g->members.data + group->start; - - g->current_group = gi; - for (i32 k = 0; k < group->count; k++) { - Entity *e = g->nodes[members[k]]; - if (e->flags & EntityFlag_Lazy) { - // NOTE: only checked when something uses it; the group orders it after what it names - continue; - } - g->current_entity = e; - GlobalEntityTimingFrame timing_frame = global_entity_timing_begin(e); - check_single_global_entity(c, e, e->decl_info); - if (e->type != nullptr && is_type_typed(e->type)) { - for (Type *t = nullptr; mpsc_dequeue(&c->soa_types_to_complete, &t); /**/) { - complete_soa_type(c, t, false); - } - - (void)type_size_of(e->type); - (void)type_align_of(e->type); - } - global_entity_timing_end(timing_frame, e); - } - group->done = true; - g->current_group = -1; - g->current_entity = nullptr; -} - -// Groups in dependency order; with `-internal-shuffle-global-entities`, a random one of the groups whose -// dependencies are done, as a parallel checker might -gb_internal void check_global_groups(Checker *c, GlobalGroupGraph *g) { - i32 group_count = cast(i32)g->groups.count; - u64 seed = build_context.internal_shuffle_global_entities; - if (seed == 0) { - for (i32 gi = 0; gi < group_count; gi++) { - check_global_group(c, g, gi); - } - return; - } - - auto dependents = array_make >(heap_allocator(), group_count); - auto dep_count = array_make (heap_allocator(), group_count); - auto seen = array_make (heap_allocator(), group_count); - auto ready = array_make (heap_allocator(), 0, group_count); - defer ({ - for (auto &d : dependents) { - array_free(&d); - } - array_free(&dependents); - }); - defer (array_free(&dep_count)); - defer (array_free(&seen)); - defer (array_free(&ready)); - - for (i32 gi = 0; gi < group_count; gi++) { - dep_count[gi] = 0; - dependents[gi] = {}; - seen[gi] = -1; - } - for (i32 gi = 0; gi < group_count; gi++) { - GlobalGroup const &group = g->groups[gi]; - for (i32 k = 0; k < group.count; k++) { - i32 v = g->members[group.start + k]; - for (i32 i = g->offsets[v]; i < g->offsets[v+1]; i++) { - i32 dep = g->group_of[g->targets[i]]; - if (dep != gi && seen[dep] != gi) { - seen[dep] = gi; - dep_count[gi] += 1; - if (dependents[dep].allocator.proc == nullptr) { - array_init(&dependents[dep], heap_allocator()); - } - array_add(&dependents[dep], gi); - } - } - } - if (dep_count[gi] == 0) { - array_add(&ready, gi); - } - } - - u64 state = seed; - isize checked = 0; - while (ready.count > 0) { - isize i = cast(isize)(global_group_random(&state) % cast(u64)ready.count); - i32 gi = ready[i]; - ready[i] = ready[ready.count-1]; - array_pop(&ready); - - check_global_group(c, g, gi); - checked += 1; - for (i32 next : dependents[gi]) { - if (--dep_count[next] == 0) { - array_add(&ready, next); - } - } - } - GB_ASSERT(checked == group_count); -} - -gb_internal void destroy_global_groups(GlobalGroupGraph *g) { - array_free(&g->nodes); - map_destroy(&g->node_of); - array_free(&g->offsets); - array_free(&g->targets); - array_free(&g->group_of); - array_free(&g->groups); - array_free(&g->members); -} - -// -internal-global-entity-graph: the graph the groups come from, weighted by the measured self times -gb_internal void print_global_groups(GlobalGroupGraph *g) { - u64 const freq = time_stamp__freq(); - i32 group_count = cast(i32)g->groups.count; - - auto ticks = array_make(heap_allocator(), group_count); - auto path = array_make(heap_allocator(), group_count); - auto seen = array_make(heap_allocator(), group_count); - defer (array_free(&ticks)); - defer (array_free(&path)); - defer (array_free(&seen)); - - u64 total = 0; - i32 largest = 0; - isize cyclic = 0; - mutex_lock(&global_entity_time_mutex); - for (i32 gi = 0; gi < group_count; gi++) { - GlobalGroup const &group = g->groups[gi]; - ticks[gi] = 0; - seen[gi] = -1; - for (i32 k = 0; k < group.count; k++) { - GlobalEntityTime *t = map_get(&global_entity_times, g->nodes[g->members[group.start + k]]); - ticks[gi] += t ? t->ticks : 0; - } - total += ticks[gi]; - largest = gb_max(largest, group.count); - cyclic += group.count > 1; - } - mutex_unlock(&global_entity_time_mutex); - - u64 critical = 0; - for (i32 gi = 0; gi < group_count; gi++) { - GlobalGroup const &group = g->groups[gi]; - u64 longest = 0; - for (i32 k = 0; k < group.count; k++) { - i32 v = g->members[group.start + k]; - for (i32 i = g->offsets[v]; i < g->offsets[v+1]; i++) { - i32 dep = g->group_of[g->targets[i]]; - if (dep != gi && seen[dep] != gi) { - seen[dep] = gi; - longest = gb_max(longest, path[dep]); - } - } - } - path[gi] = ticks[gi] + longest; - critical = gb_max(critical, path[gi]); - } - - f64 total_ms = global_graph_ms(total, freq); - f64 critical_ms = global_graph_ms(critical, freq); - gb_printf_err("Global groups (syntactic graph, as scheduled)\n"); - gb_printf_err(" nodes: %td, edges: %td, groups: %d (%td with a cycle), largest has %d entities\n", - g->nodes.count, g->targets.count, group_count, cyclic, largest); - gb_printf_err(" critical path: %.3f ms of %.3f ms -> at most %.2fx speedup\n", - critical_ms, total_ms, critical_ms > 0 ? total_ms/critical_ms : 0.0); - gb_printf_err(" missing edges: %td\n", g->missing_edges); -} - -gb_internal void check_all_global_entities(Checker *c) { - in_single_threaded_checker_stage.store(true, std::memory_order_relaxed); - - // NOTE(bill): the runtime types the checker looks up by name rather than through a declaration - init_preload(c); - { - u32 hash = 0; - InternedString name = string_interner_insert(str_lit("Load_Directory_File"), 0, &hash); - if (scope_lookup_current(c->info.runtime_package->scope, name, hash) != nullptr) { - init_core_load_directory_file(c); - } - } - - TIME_SECTION("check all global entities - build groups"); - GlobalGroupGraph *g = &global_groups; - build_global_groups(c, g); - - TIME_SECTION("check all global entities - check groups"); - g->active = true; - g->current_group = -1; - check_global_groups(c, g); - g->active = false; - - if (build_context.internal_check_global_edges && g->missing_edges > 0) { - gb_printf_err("%td missing global dependencies\n", g->missing_edges); - gb_exit(1); - } - - in_single_threaded_checker_stage.store(false, std::memory_order_relaxed); -} diff --git a/src/checker_global_when.cpp b/src/checker_global_when.cpp deleted file mode 100644 index 2b4187b68..000000000 --- a/src/checker_global_when.cpp +++ /dev/null @@ -1,474 +0,0 @@ -// Global 'when's and 'foreign' blocks: every name one may declare is a placeholder in its scope, and the -// first lookup of a placeholder resolves them, so the order of files and declarations does not matter - -struct GlobalDeclSource { - Ast * node; // WhenStmt or ForeignBlockDecl - AstFile * file; - GlobalDeclSource *parent; - bool in_else; // within the else branch of `parent` - bool reachable; - bool reported_cycle; - EntityState state; - ForeignContext foreign_context; // of a resolved 'foreign' block -}; - -struct GlobalDeclSourceFrame { - GlobalDeclSource *source; - InternedString needs; // the placeholder being resolved for it -}; - -gb_global Array global_decl_sources; -gb_global Array global_decl_source_stack; -gb_global Array global_placeholder_scopes; -gb_global CheckerContext global_decl_source_export_ctx; -gb_global UntypedExprInfoMap global_decl_source_export_untyped; - -enum : u8 { - PlaceholderScope_File = 1<<0, - PlaceholderScope_Pkg = 1<<1, -}; - -// -1 when 'private' has a value that is not a string literal -gb_internal i32 syntactic_visibility(Array const &attributes) { - for (Ast *attr : attributes) { - if (attr->kind != Ast_Attribute) { - continue; - } - for (Ast *elem : attr->Attribute.elems) { - if (elem->kind == Ast_Ident && elem->Ident.token.string == "private") { - return EntityVisiblity_PrivateToPackage; - } - if (elem->kind == Ast_FieldValue && - elem->FieldValue.field->kind == Ast_Ident && - elem->FieldValue.field->Ident.token.string == "private") { - Ast *value = elem->FieldValue.value; - if (value != nullptr && value->tav.value.kind == ExactValue_String) { - return value->tav.value.value_string == "file" ? EntityVisiblity_PrivateToFile : EntityVisiblity_PrivateToPackage; - } - return -1; - } - } - } - return EntityVisiblity_Public; -} - -gb_internal bool has_syntactic_attribute(Array const &attributes, String const &name) { - for (Ast *attr : attributes) { - if (attr->kind != Ast_Attribute) { - continue; - } - for (Ast *elem : attr->Attribute.elems) { - Ast *field = elem->kind == Ast_FieldValue ? elem->FieldValue.field : elem; - if (field->kind == Ast_Ident && field->Ident.token.string == name) { - return true; - } - } - } - return false; -} - -gb_internal void add_placeholder(Scope *s, InternedString name, GlobalDeclSource *src) { - if (name.value == 0 || name.is_blank()) { - return; - } - if (s->placeholders == nullptr) { - s->placeholders = permanent_alloc_item>(); - map_init(s->placeholders); - array_add(&global_placeholder_scopes, s); - } - u64 key = name.value; - for (auto *e = multi_map_find_first(s->placeholders, key); e != nullptr; e = multi_map_find_next(s->placeholders, e)) { - if (e->value == src) { - return; - } - } - multi_map_insert(s->placeholders, key, src); -} - -gb_internal void add_placeholders(AstFile *f, u8 scopes, InternedString name, GlobalDeclSource *src) { - if (scopes & PlaceholderScope_File) { - add_placeholder(f->scope, name, src); - } - if (scopes & PlaceholderScope_Pkg) { - add_placeholder(f->pkg->scope, name, src); - } -} - -gb_internal GlobalDeclSource *add_global_decl_source(Ast *node, AstFile *f, GlobalDeclSource *parent, bool in_else) { - GlobalDeclSource *src = permanent_alloc_item(); - src->node = node; - src->file = f; - src->parent = parent; - src->in_else = in_else; - src->reachable = true; - src->state = EntityState_Unresolved; - array_add(&global_decl_sources, src); - return src; -} - -gb_internal void scan_global_decl_sources(AstFile *f, Slice const &stmts, GlobalDeclSource *owner, bool in_else, i32 foreign_visibility); - -gb_internal void scan_global_when_stmt(AstFile *f, Ast *node, GlobalDeclSource *parent, bool in_else, i32 foreign_visibility) { - ast_node(ws, WhenStmt, node); - GlobalDeclSource *src = add_global_decl_source(node, f, parent, in_else); - if (ws->body != nullptr && ws->body->kind == Ast_BlockStmt) { - scan_global_decl_sources(f, ws->body->BlockStmt.stmts, src, false, foreign_visibility); - } - if (ws->else_stmt != nullptr) { - switch (ws->else_stmt->kind) { - case Ast_BlockStmt: - scan_global_decl_sources(f, ws->else_stmt->BlockStmt.stmts, src, true, foreign_visibility); - break; - case Ast_WhenStmt: - scan_global_when_stmt(f, ws->else_stmt, src, true, foreign_visibility); - break; - } - } -} - -gb_internal void scan_global_decl_sources(AstFile *f, Slice const &stmts, GlobalDeclSource *owner, bool in_else, i32 foreign_visibility) { - // NOTE(bill): `owner == nullptr` is the file scope itself, whose other declarations are already collected - for (Ast *decl : stmts) { - switch (decl->kind) { - case_ast_node(vd, ValueDecl, decl); - if (owner == nullptr) { - break; - } - i32 visibility = syntactic_visibility(vd->attributes); - if (visibility == EntityVisiblity_Public) { - visibility = foreign_visibility; - } - if (visibility == EntityVisiblity_Public && (f->flags & AstFile_IsPrivateFile)) { - visibility = EntityVisiblity_PrivateToFile; - } - u8 scopes = PlaceholderScope_Pkg; - if (visibility == EntityVisiblity_PrivateToFile) { - scopes = PlaceholderScope_File; - } else if (visibility < 0) { - scopes = PlaceholderScope_File|PlaceholderScope_Pkg; - } - for (Ast *name : vd->names) { - if (name->kind == Ast_Ident) { - add_placeholders(f, scopes, name->Ident.interned, owner); - } - } - case_end; - - case_ast_node(fl, ForeignImportDecl, decl); - if (owner == nullptr) { - break; - } - String library_name = fl->library_name.string; - if (library_name.len == 0 && fl->fullpaths.count != 0) { - library_name = path_to_entity_name(fl->library_name.string, fl->fullpaths[0]); - } - if (library_name.len != 0) { - u8 scopes = has_syntactic_attribute(fl->attributes, str_lit("export")) ? PlaceholderScope_Pkg : PlaceholderScope_File; - add_placeholders(f, scopes, string_interner_insert(library_name), owner); - } - case_end; - - case_ast_node(fb, ForeignBlockDecl, decl); - GlobalDeclSource *src = add_global_decl_source(decl, f, owner, in_else); - if (fb->body != nullptr && fb->body->kind == Ast_BlockStmt) { - scan_global_decl_sources(f, fb->body->BlockStmt.stmts, src, false, syntactic_visibility(fb->attributes)); - } - case_end; - - case_ast_node(ws, WhenStmt, decl); - scan_global_when_stmt(f, decl, owner, in_else, foreign_visibility); - case_end; - - case_ast_node(es, ExprStmt, decl); - if (owner == nullptr && es->expr->kind == Ast_CallExpr && - es->expr->CallExpr.proc->kind == Ast_BasicDirective && - (decl->state_flags & StateFlag_BeenHandled) == 0) { - decl->state_flags |= StateFlag_BeenHandled; - array_add(&f->delayed_decls_queues[AstDelayQueue_Expr], es->expr); - } - case_end; - } - } -} - -gb_internal bool is_global_decl_source_in_when(GlobalDeclSource *src) { - for (; src != nullptr; src = src->parent) { - if (src->node->kind == Ast_WhenStmt) { - return true; - } - } - return false; -} - -gb_internal Slice global_decl_source_taken_stmts(GlobalDeclSource *src) { - if (src->node->kind == Ast_ForeignBlockDecl) { - Ast *body = src->node->ForeignBlockDecl.body; - if (body != nullptr && body->kind == Ast_BlockStmt) { - return body->BlockStmt.stmts; - } - return {}; - } - ast_node(ws, WhenStmt, src->node); - if (ws->determined_cond) { - if (ws->body != nullptr && ws->body->kind == Ast_BlockStmt) { - return ws->body->BlockStmt.stmts; - } - } else if (ws->else_stmt != nullptr && ws->else_stmt->kind == Ast_BlockStmt) { - return ws->else_stmt->BlockStmt.stmts; - } - return {}; -} - -gb_internal void collect_global_decl_source_stmts(CheckerContext *ctx, Slice const &stmts) { - AstFile *f = ctx->file; - for (Ast *decl : stmts) { - if (decl->kind == Ast_ValueDecl) { - check_collect_value_decl(ctx, decl); - } - } - check_export_entities_in_pkg(&global_decl_source_export_ctx, f->pkg, &global_decl_source_export_untyped); - - // NOTE(bill): after the value declarations, as their attributes are evaluated - for (Ast *decl : stmts) { - switch (decl->kind) { - case_ast_node(fl, ForeignImportDecl, decl); - check_add_foreign_import_decl(ctx, decl); - case_end; - - case_ast_node(es, ExprStmt, decl); - if (es->expr->kind == Ast_CallExpr && es->expr->CallExpr.proc->kind == Ast_BasicDirective && - (decl->state_flags & StateFlag_BeenHandled) == 0) { - decl->state_flags |= StateFlag_BeenHandled; - array_add(&f->delayed_decls_queues[AstDelayQueue_Expr], es->expr); - } - case_end; - } - } -} - -gb_internal Token global_decl_source_token(GlobalDeclSource *src) { - if (src->node->kind == Ast_WhenStmt) { - return src->node->WhenStmt.token; - } - return src->node->ForeignBlockDecl.token; -} - -gb_internal void report_global_decl_source_cycle(GlobalDeclSource *src, InternedString needed) { - if (src->reported_cycle) { - return; - } - src->reported_cycle = true; - - isize start = 0; - for (isize i = global_decl_source_stack.count-1; i >= 0; i--) { - if (global_decl_source_stack[i].source == src) { - start = i; - break; - } - } - - ERROR_BLOCK(); - Token token = global_decl_source_token(src); - error(token, "Cyclic dependency between global '%.*s' declarations", LIT(token.string)); - for (isize i = start; i < global_decl_source_stack.count; i++) { - Token t = global_decl_source_token(global_decl_source_stack[i].source); - InternedString name = i+1 < global_decl_source_stack.count ? global_decl_source_stack[i].needs : needed; - error_line("\t'%.*s' at %s needs '%s', which may be declared by\n", LIT(t.string), token_pos_to_string(t.pos), name.cstring()); - } - error_line("\t'%.*s' at %s\n", LIT(token.string), token_pos_to_string(token.pos)); -} - -gb_internal void resolve_global_decl_source(GlobalDeclSource *src, InternedString needed) { - if (src->state == EntityState_Resolved) { - return; - } - if (src->state == EntityState_InProgress) { - report_global_decl_source_cycle(src, needed); - return; - } - - GlobalDeclSource *foreign_block = nullptr; - if (src->parent != nullptr) { - GlobalDeclSource *parent = src->parent; - resolve_global_decl_source(parent, needed); - if (parent->state != EntityState_Resolved) { - return; - } - bool reachable = parent->reachable; - if (parent->node->kind == Ast_WhenStmt) { - reachable = reachable && parent->node->WhenStmt.determined_cond != src->in_else; - } - if (!reachable) { - src->reachable = false; - src->state = EntityState_Resolved; - return; - } - for (GlobalDeclSource *p = parent; p != nullptr; p = p->parent) { - if (p->node->kind == Ast_ForeignBlockDecl) { - foreign_block = p; - break; - } - } - } - - src->state = EntityState_InProgress; - array_add(&global_decl_source_stack, GlobalDeclSourceFrame{src, {}}); - - CheckerContext ctx = {}; - init_checker_context(&ctx, global_checker_ptr.load(std::memory_order_relaxed)); - UntypedExprInfoMap untyped = {}; - reset_checker_context(&ctx, src->file, &untyped); - if (foreign_block != nullptr) { - ctx.foreign_context = foreign_block->foreign_context; - } - - if (src->node->kind == Ast_WhenStmt) { - ast_node(ws, WhenStmt, src->node); - Operand operand = {Addressing_Invalid}; - check_expr(&ctx, &operand, ws->cond); - if (operand.mode != Addressing_Invalid && !is_type_boolean(operand.type)) { - error(ws->cond, "Non-boolean condition in 'when' statement"); - } - if (operand.mode != Addressing_Constant) { - error(ws->cond, "Non-constant condition in 'when' statement"); - } - ws->is_cond_determined = true; - ws->determined_cond = operand.value.kind == ExactValue_Bool && operand.value.value_bool; - if (ws->body == nullptr || ws->body->kind != Ast_BlockStmt) { - error(ws->cond, "Invalid body for 'when' statement"); - } else if (ws->else_stmt != nullptr && ws->else_stmt->kind != Ast_BlockStmt && ws->else_stmt->kind != Ast_WhenStmt) { - error(ws->else_stmt, "Invalid 'else' statement in 'when' statement"); - } - } else { - ast_node(fb, ForeignBlockDecl, src->node); - if (fb->foreign_library->kind == Ast_Ident) { - ctx.foreign_context.curr_library = fb->foreign_library; - } else { - error(fb->foreign_library, "Foreign block name must be an identifier or 'export'"); - ctx.foreign_context.curr_library = nullptr; - } - check_decl_attributes(&ctx, fb->attributes, foreign_block_decl_attribute, nullptr); - src->foreign_context = ctx.foreign_context; - } - - // NOTE: resolved before its declarations are collected, which evaluates the attributes of 'foreign import's - src->state = EntityState_Resolved; - array_pop(&global_decl_source_stack); - - collect_global_decl_source_stmts(&ctx, global_decl_source_taken_stmts(src)); - - add_untyped_expressions(ctx.info, &untyped); - map_destroy(&untyped); - destroy_checker_context(&ctx); -} - -gb_internal Entity *force_scope_placeholders(Scope *s, InternedString name, u32 hash) { - PtrMap *m = s->placeholders; - bool forced = false; - for (auto *e = multi_map_find_first(m, cast(u64)name.value); e != nullptr; e = multi_map_find_next(m, e)) { - GlobalDeclSource *src = e->value; - if (src->state != EntityState_Resolved) { - if (global_decl_source_stack.count > 0) { - global_decl_source_stack[global_decl_source_stack.count-1].needs = name; - } - resolve_global_decl_source(src, name); - forced = true; - } - } - if (!forced) { - return nullptr; - } - rw_mutex_shared_lock(&s->mutex); - Entity *found = scope_map_get(&s->elements, name, hash); - rw_mutex_shared_unlock(&s->mutex); - return found; -} - -gb_internal void check_vet_when_shadowing_entity(Entity *e) { - if (e == nullptr || e->scope == nullptr || (e->scope->flags & ScopeFlag_File) == 0) { - return; - } - InternedString name = entity_interned_name(e); - u32 hash = e->interned_name_hash.load(std::memory_order_relaxed); - Scope *outer = e->scope->parent; - if (scope_map_get(&e->scope->elements, name, hash) != e) { - outer = outer->parent; // in the package scope - } - if (outer == nullptr) { - return; - } - Entity *shadowed = scope_lookup(outer, name, hash); - if (shadowed == nullptr || shadowed == e) { - return; - } - if (shadowed->scope == builtin_pkg->scope) { - error(e->token, "Declaration of '%.*s' within a global 'when' shadows the builtin '%.*s'", LIT(e->token.string), LIT(e->token.string)); - } else { - error(e->token, "Declaration of '%.*s' within a global 'when' shadows the declaration at %s", LIT(e->token.string), token_pos_to_string(shadowed->token.pos)); - } -} - -gb_internal void check_vet_when_shadowing(void) { - for (GlobalDeclSource *src : global_decl_sources) { - if (!src->reachable || src->state != EntityState_Resolved) { - continue; - } - if ((ast_file_vet_flags(src->file) & VetFlag_WhenShadowing) == 0 || !is_global_decl_source_in_when(src)) { - continue; - } - for (Ast *decl : global_decl_source_taken_stmts(src)) { - if (decl->kind == Ast_ValueDecl) { - for (Ast *name : decl->ValueDecl.names) { - if (name->kind == Ast_Ident) { - check_vet_when_shadowing_entity(name->Ident.entity.load()); - } - } - } else if (decl->kind == Ast_ForeignImportDecl) { - Token token = decl->ForeignImportDecl.library_name; - InternedString name = string_interner_insert(token.string); - for (Scope *s = src->file->scope; s != nullptr && s != builtin_pkg->scope; s = s->parent) { - Entity *e = scope_map_get(&s->elements, name, name.hash()); - if (e != nullptr && e->kind == Entity_LibraryName && e->LibraryName.decl == decl) { - check_vet_when_shadowing_entity(e); - break; - } - } - } - } - } -} - -// Placeholders for every file, then every source resolved in package, file and source order, which -// only matters for which errors are reported -gb_internal void resolve_global_decl_sources(Checker *c, Array const &package_order) { - array_init(&global_decl_sources, heap_allocator()); - array_init(&global_decl_source_stack, heap_allocator()); - array_init(&global_placeholder_scopes, heap_allocator()); - init_checker_context(&global_decl_source_export_ctx, c); - defer (destroy_checker_context(&global_decl_source_export_ctx)); - - u64 stage_start = global_import_stage_begin(); - for (ImportGraphNode *node : package_order) { - for (AstFile *f : node->pkg->files) { - scan_global_decl_sources(f, f->decls, nullptr, false, EntityVisiblity_Public); - } - } - global_import_stage_end(GlobalImportStage_Placeholders, stage_start); - - stage_start = global_import_stage_begin(); - for (GlobalDeclSource *src : global_decl_sources) { - resolve_global_decl_source(src, {}); - } - GB_ASSERT(global_decl_source_stack.count == 0); - - for (Scope *s : global_placeholder_scopes) { - map_destroy(s->placeholders); - s->placeholders = nullptr; - } - array_clear(&global_placeholder_scopes); - - check_vet_when_shadowing(); - global_import_stage_end(GlobalImportStage_DeclSources, stage_start); - - map_destroy(&global_decl_source_export_untyped); -} From 66c4fe1d4990359501bd327635bfa717e59227a2 Mon Sep 17 00:00:00 2001 From: gingerBill Date: Thu, 1 Oct 2026 18:49:51 +0100 Subject: [PATCH 07/17] Decide cycles of global `when`s by trying every choice of their branches; ambiguous or contradictory cycles are errors --- src/check_decl.cpp | 5 + src/checker.cpp | 35 +- src/checker.hpp | 9 + src/checker_global.cpp | 820 ++++++++++++++++-- tests/issues/run.bat | 4 +- tests/issues/run.sh | 9 +- .../issues/test_issue_global_when_cycle.odin | 8 +- 7 files changed, 829 insertions(+), 61 deletions(-) diff --git a/src/check_decl.cpp b/src/check_decl.cpp index fd9672f21..3343465ae 100644 --- a/src/check_decl.cpp +++ b/src/check_decl.cpp @@ -2140,6 +2140,11 @@ gb_internal void check_entity_decl(CheckerContext *ctx, Entity *e, DeclInfo *d, if (e->state == EntityState_Resolved) { return; } + GlobalWhenTrialEntityScope trial_scope = {}; + if (global_when_trial != nullptr && !global_when_trial_begin_entity(e, &trial_scope)) { + return; + } + defer (global_when_trial_end_entity(&trial_scope)); bool is_lazy = (e->flags & EntityFlag_Lazy) != 0; if (is_lazy) { mutex_lock(&ctx->info->lazy_mutex); diff --git a/src/checker.cpp b/src/checker.cpp index 7acd8f3f8..6bc02b9f6 100644 --- a/src/checker.cpp +++ b/src/checker.cpp @@ -387,8 +387,8 @@ gb_internal void check_open_scope(CheckerContext *c, Ast *node) { break; } if (c->decl && c->decl->proc_lit) { - // NOTE: numbered by position rather than in checking order, as that order varies (e.g. with - // which caller instantiates a record first); 0 is the procedure's own scope + // NOTE(bill): numbered by position rather than in checking order, as that order varies + // (e.g. with which caller instantiates a record first); 0 is the procedure's own scope scope->index = node->kind == Ast_ProcType ? 0 : 1 + ast_token(node).pos.offset; } c->scope = scope; @@ -402,12 +402,19 @@ gb_internal void check_close_scope(CheckerContext *c) { gb_internal Entity *force_scope_placeholders(Scope *s, InternedString name, u32 hash); +struct GlobalWhenTrial; +gb_global GlobalWhenTrial *global_when_trial; +gb_internal Entity *global_when_trial_lookup(Scope *s, InternedString name, u32 hash, Entity *found); + gb_internal Entity *scope_lookup_current(Scope *s, InternedString name, u32 hash) { // Entity **found = string_map_get(&s->elements, name); if (hash == 0) { hash = name.hash(); } Entity *found = scope_map_get(&s->elements, name, hash); + if (global_when_trial != nullptr) { + found = global_when_trial_lookup(s, name, hash, found); + } if (found) { return found; } @@ -433,6 +440,9 @@ gb_internal void scope_lookup_parent(Scope *scope, InternedString name, Scope ** if (!is_single_threaded) rw_mutex_shared_lock(&s->mutex); found = scope_map_get(&s->elements, name, hash); if (!is_single_threaded) rw_mutex_shared_unlock(&s->mutex); + if (global_when_trial != nullptr) { + found = global_when_trial_lookup(s, name, hash, found); + } if (found == nullptr && s->placeholders != nullptr) { found = force_scope_placeholders(s, name, hash); } @@ -2093,8 +2103,8 @@ gb_internal bool redeclaration_error(String name, Entity *prev, Entity *found) { // NOTE(bill): Error should have been handled already return false; } - // NOTE: the insertion order is a race between the files of a package, so order the pair by - // position; the later declaration stays the anchor, as it is the one being reported + // NOTE(bill): the insertion order is a race between the files of a package, so order the pair by position; + // the later declaration stays the anchor, as it is the one being reported TokenPos first = prev->token.pos; TokenPos second = pos; if (second < first) { @@ -2276,6 +2286,19 @@ gb_internal void add_entity_and_decl_info(CheckerContext *c, Ast *identifier, En e->flags &= ~EntityFlag_Lazy; } + if (c->trial_entities != nullptr) { + // NOTE(bill): a scratch copy for a global 'when' trial, put in no scope or queue + e->flags &= ~EntityFlag_Lazy; + e->file = c->file; + e->decl_info = d; + e->pkg = c->pkg; + d->entity.store(e); + identifier->Ident.entity = e; + e->identifier = identifier; + array_add(c->trial_entities, e); + return; + } + if (e->scope != nullptr) { Scope *scope = e->scope; @@ -5124,7 +5147,9 @@ gb_internal void check_collect_value_decl(CheckerContext *c, Ast *decl) { } } - check_builtin_attributes(c, e, &d->attributes); + if (c->trial_entities == nullptr) { + check_builtin_attributes(c, e, &d->attributes); + } bool is_exported = entity_visibility_kind != EntityVisiblity_PrivateToFile; add_entity_and_decl_info(c, name, e, d, is_exported); diff --git a/src/checker.hpp b/src/checker.hpp index 05eed75ef..633790405 100644 --- a/src/checker.hpp +++ b/src/checker.hpp @@ -850,6 +850,8 @@ struct CheckerContext { u8 bit_field_bit_size; Scope * polymorphic_scope; + Array *trial_entities; // global declarations are collected here only, for a global 'when' trial + Ast *assignment_lhs_hint; Ast *asm_template_hint; }; @@ -919,6 +921,13 @@ gb_internal void check_add_foreign_import_decl(CheckerContext *c, Ast *decl); gb_internal void check_entity_decl(CheckerContext *c, Entity *e, DeclInfo *d, Type *named_type); gb_internal void global_group_check_edge(CheckerContext *ctx, Entity *e); +struct GlobalWhenTrialEntityScope { + struct GlobalWhenTrial *trial; + i32 mute_depth; +}; +gb_internal bool global_when_trial_begin_entity(Entity *e, GlobalWhenTrialEntityScope *scope); +gb_internal void global_when_trial_end_entity(GlobalWhenTrialEntityScope *scope); + // -internal-global-entity-graph struct GlobalEntityTimingFrame { u64 start; diff --git a/src/checker_global.cpp b/src/checker_global.cpp index cf9bb6e84..ccc212505 100644 --- a/src/checker_global.cpp +++ b/src/checker_global.cpp @@ -83,17 +83,61 @@ gb_internal void global_entity_timing_end(GlobalEntityTimingFrame const &f, Enti // Global 'when's and 'foreign' blocks: every name one may declare is a placeholder in its scope, and the // first lookup of a placeholder resolves them, so the order of files and declarations does not matter +struct GlobalDeclSourceName { + InternedString name; + Scope * scope; + Ast * decl; // ValueDecl or ForeignImportDecl + bool in_else; // within the else branch of a 'when' +}; + +struct GlobalWhenCycle; + struct GlobalDeclSource { Ast * node; // WhenStmt or ForeignBlockDecl AstFile * file; GlobalDeclSource *parent; - bool in_else; // within the else branch of `parent` + bool in_else; bool reachable; bool reported_cycle; EntityState state; - ForeignContext foreign_context; // of a resolved 'foreign' block + ForeignContext foreign_context; + + Array names; + GlobalWhenCycle * cycle; + i32 cycle_index; + bool predetermined; + bool predetermined_cond; }; +// A possible cycle between global 'when's, found from syntax; the branches are chosen by trying every +// combination, see `search_global_when_cycle` +struct GlobalWhenCycle { + Array sources; // in source order + PtrSet decls; // declarations in the cycle, whose checking depends on the choice + bool searched; +}; + +// One condition evaluated for one choice of branches: lookups see the declarations of the chosen +// branches, and scratch copies of the declarations in `cycle->decls` +struct GlobalWhenTrial { + GlobalWhenCycle * cycle; + u32 mask; // bit i: `cycle->sources[i]` takes its first branch + u32 reachable; + u32 used; // sources whose chosen branch a lookup found + isize real_depth; // within the check of an entity outside the trial + bool unsupported; + bool broken; // that entity reached the cycle, which the graph missed + PtrMap *> decl_entities; + PtrSet scratch; +}; + +gb_global isize global_when_cycle_count; +gb_global isize global_when_cycle_sources; +gb_global isize global_when_trial_count; + +gb_internal void find_global_when_cycles(void); +gb_internal void search_global_when_cycle(GlobalWhenCycle *cycle); + struct GlobalDeclSourceFrame { GlobalDeclSource *source; InternedString needs; // the placeholder being resolved for it @@ -167,12 +211,20 @@ gb_internal void add_placeholder(Scope *s, InternedString name, GlobalDeclSource multi_map_insert(s->placeholders, key, src); } -gb_internal void add_placeholders(AstFile *f, u8 scopes, InternedString name, GlobalDeclSource *src) { +gb_internal void add_placeholders(AstFile *f, u8 scopes, InternedString name, GlobalDeclSource *src, Ast *decl, bool in_else) { + if (name.value == 0 || name.is_blank()) { + return; + } + if (src->names.allocator.proc == nullptr) { + array_init(&src->names, heap_allocator()); + } if (scopes & PlaceholderScope_File) { add_placeholder(f->scope, name, src); + array_add(&src->names, GlobalDeclSourceName{name, f->scope, decl, in_else}); } if (scopes & PlaceholderScope_Pkg) { add_placeholder(f->pkg->scope, name, src); + array_add(&src->names, GlobalDeclSourceName{name, f->pkg->scope, decl, in_else}); } } @@ -231,7 +283,7 @@ gb_internal void scan_global_decl_sources(AstFile *f, Slice const &stmts, } for (Ast *name : vd->names) { if (name->kind == Ast_Ident) { - add_placeholders(f, scopes, name->Ident.interned, owner); + add_placeholders(f, scopes, name->Ident.interned, owner, decl, in_else); } } case_end; @@ -246,7 +298,7 @@ gb_internal void scan_global_decl_sources(AstFile *f, Slice const &stmts, } if (library_name.len != 0) { u8 scopes = has_syntactic_attribute(fl->attributes, str_lit("export")) ? PlaceholderScope_Pkg : PlaceholderScope_File; - add_placeholders(f, scopes, string_interner_insert(library_name), owner); + add_placeholders(f, scopes, string_interner_insert(library_name), owner, decl, in_else); } case_end; @@ -360,14 +412,19 @@ gb_internal void report_global_decl_source_cycle(GlobalDeclSource *src, Interned error_line("\t'%.*s' at %s\n", LIT(token.string), token_pos_to_string(token.pos)); } -gb_internal void resolve_global_decl_source(GlobalDeclSource *src, InternedString needed) { - if (src->state == EntityState_Resolved) { - return; - } +gb_internal void resolve_global_decl_source(GlobalDeclSource *src, InternedString needed); + +gb_internal void resolve_global_decl_source_internal(GlobalDeclSource *src, InternedString needed) { if (src->state == EntityState_InProgress) { report_global_decl_source_cycle(src, needed); return; } + if (src->cycle != nullptr && !src->cycle->searched) { + search_global_when_cycle(src->cycle); + if (src->state == EntityState_Resolved) { + return; + } + } GlobalDeclSource *foreign_block = nullptr; if (src->parent != nullptr) { @@ -406,16 +463,21 @@ gb_internal void resolve_global_decl_source(GlobalDeclSource *src, InternedStrin if (src->node->kind == Ast_WhenStmt) { ast_node(ws, WhenStmt, src->node); - Operand operand = {Addressing_Invalid}; - check_expr(&ctx, &operand, ws->cond); - if (operand.mode != Addressing_Invalid && !is_type_boolean(operand.type)) { - error(ws->cond, "Non-boolean condition in 'when' statement"); + if (src->predetermined) { + ws->is_cond_determined = true; + ws->determined_cond = src->predetermined_cond; + } else { + Operand operand = {Addressing_Invalid}; + check_expr(&ctx, &operand, ws->cond); + if (operand.mode != Addressing_Invalid && !is_type_boolean(operand.type)) { + error(ws->cond, "Non-boolean condition in 'when' statement"); + } + if (operand.mode != Addressing_Constant) { + error(ws->cond, "Non-constant condition in 'when' statement"); + } + ws->is_cond_determined = true; + ws->determined_cond = operand.value.kind == ExactValue_Bool && operand.value.value_bool; } - if (operand.mode != Addressing_Constant) { - error(ws->cond, "Non-constant condition in 'when' statement"); - } - ws->is_cond_determined = true; - ws->determined_cond = operand.value.kind == ExactValue_Bool && operand.value.value_bool; if (ws->body == nullptr || ws->body->kind != Ast_BlockStmt) { error(ws->cond, "Invalid body for 'when' statement"); } else if (ws->else_stmt != nullptr && ws->else_stmt->kind != Ast_BlockStmt && ws->else_stmt->kind != Ast_WhenStmt) { @@ -433,7 +495,7 @@ gb_internal void resolve_global_decl_source(GlobalDeclSource *src, InternedStrin src->foreign_context = ctx.foreign_context; } - // NOTE: resolved before its declarations are collected, which evaluates the attributes of 'foreign import's + // NOTE(bill): resolved before its declarations are collected, which evaluates the attributes of 'foreign import's src->state = EntityState_Resolved; array_pop(&global_decl_source_stack); @@ -444,11 +506,28 @@ gb_internal void resolve_global_decl_source(GlobalDeclSource *src, InternedStrin destroy_checker_context(&ctx); } +gb_internal void resolve_global_decl_source(GlobalDeclSource *src, InternedString needed) { + if (src->state == EntityState_Resolved) { + return; + } + GlobalWhenTrial *trial = global_when_trial; + i32 mute_depth = global_error_mute_depth; + global_when_trial = nullptr; + global_error_mute_depth = 0; + resolve_global_decl_source_internal(src, needed); + global_when_trial = trial; + global_error_mute_depth = mute_depth; +} + gb_internal Entity *force_scope_placeholders(Scope *s, InternedString name, u32 hash) { PtrMap *m = s->placeholders; bool forced = false; for (auto *e = multi_map_find_first(m, cast(u64)name.value); e != nullptr; e = multi_map_find_next(m, e)) { GlobalDeclSource *src = e->value; + if (global_when_trial != nullptr && src->cycle == global_when_trial->cycle) { + // NOTE: the trial's lookup decides what these declare + continue; + } if (src->state != EntityState_Resolved) { if (global_decl_source_stack.count > 0) { global_decl_source_stack[global_decl_source_stack.count-1].needs = name; @@ -535,6 +614,7 @@ gb_internal void resolve_global_decl_sources(Checker *c, Arraydecls, nullptr, false, EntityVisiblity_Public); } } + find_global_when_cycles(); global_import_stage_end(GlobalImportStage_Placeholders, stage_start); stage_start = global_import_stage_begin(); @@ -635,6 +715,25 @@ gb_internal i32 global_graph_scc(i32 node_count, Array const &offsets, Arra return comp_count; } +gb_internal void global_graph_csr(i32 node_count, Array const &edge_from, Array const &edge_to, Array *offsets, Array *targets) { + array_init(offsets, heap_allocator(), node_count+1); + array_init(targets, heap_allocator(), edge_to.count); + for (i32 v = 0; v <= node_count; v++) { + (*offsets)[v] = 0; + } + for (i32 from : edge_from) { + (*offsets)[from+1] += 1; + } + for (i32 v = 0; v < node_count; v++) { + (*offsets)[v+1] += (*offsets)[v]; + } + auto fill = array_clone(heap_allocator(), *offsets); + defer (array_free(&fill)); + for (isize i = 0; i < edge_from.count; i++) { + (*targets)[fill[edge_from[i]]++] = edge_to[i]; + } +} + gb_internal void global_graph_print_entity(Entity *e) { if (e == nullptr) { gb_printf_err("?"); @@ -671,11 +770,38 @@ struct GlobalGroupGraph { gb_global GlobalGroupGraph global_groups; +struct GlobalPlaceholderHit { + Scope * scope; + InternedString name; +}; + struct GlobalGraphWalk { Scope *scope; Array *refs; + Array *hits; // set: before any 'when' is resolved, a lookup passing a placeholder records it }; +gb_internal Entity *global_graph_lookup(GlobalGraphWalk *w, Scope *s, Ast *ident, bool parents) { + InternedString name = ident->Ident.interned; + u32 hash = ident->Ident.hash; + if (w->hits == nullptr) { + return parents ? scope_lookup(s, name, hash) : scope_lookup_current(s, name, hash); + } + for (; s != nullptr; s = s->parent) { + Entity *e = scope_map_get(&s->elements, name, hash); + if (e != nullptr) { + return e; + } + if (s->placeholders != nullptr && multi_map_find_first(s->placeholders, cast(u64)name.value) != nullptr) { + array_add(w->hits, GlobalPlaceholderHit{s, name}); + } + if (!parents) { + break; + } + } + return nullptr; +} + gb_internal void global_graph_walk(GlobalGraphWalk *w, Ast *node); gb_internal void global_graph_walk_slice(GlobalGraphWalk *w, Slice const &nodes) { @@ -698,16 +824,16 @@ gb_internal void global_graph_walk(GlobalGraphWalk *w, Ast *node) { } switch (node->kind) { case Ast_Ident: - global_graph_add_ref(w, scope_lookup(w->scope, node->Ident.interned, node->Ident.hash)); + global_graph_add_ref(w, global_graph_lookup(w, w->scope, node, true)); break; case Ast_SelectorExpr: { Ast *expr = node->SelectorExpr.expr; Ast *selector = node->SelectorExpr.selector; if (expr != nullptr && expr->kind == Ast_Ident) { - Entity *e = scope_lookup(w->scope, expr->Ident.interned, expr->Ident.hash); + Entity *e = global_graph_lookup(w, w->scope, expr, true); if (e != nullptr && e->kind == Entity_ImportName && selector != nullptr && selector->kind == Ast_Ident) { - global_graph_add_ref(w, scope_lookup_current(e->ImportName.scope, selector->Ident.interned, selector->Ident.hash)); + global_graph_add_ref(w, global_graph_lookup(w, e->ImportName.scope, selector, false)); } else { global_graph_add_ref(w, e); } @@ -944,11 +1070,8 @@ gb_internal void global_graph_walk(GlobalGraphWalk *w, Ast *node) { } } -gb_internal void global_graph_walk_entity(GlobalGraphWalk *w, Entity *e, DeclInfo *d) { - w->scope = d->scope; - global_graph_walk(w, d->type_expr); - global_graph_walk(w, d->init_expr); - for (Ast *attr : d->attributes) { +gb_internal void global_graph_walk_attribute_values(GlobalGraphWalk *w, Array const &attributes) { + for (Ast *attr : attributes) { if (attr->kind != Ast_Attribute) { continue; } @@ -958,6 +1081,13 @@ gb_internal void global_graph_walk_entity(GlobalGraphWalk *w, Entity *e, DeclInf } } } +} + +gb_internal void global_graph_walk_entity(GlobalGraphWalk *w, Entity *e, DeclInfo *d) { + w->scope = d->scope; + global_graph_walk(w, d->type_expr); + global_graph_walk(w, d->init_expr); + global_graph_walk_attribute_values(w, d->attributes); if (e->kind == Entity_Procedure) { global_graph_walk(w, e->Procedure.foreign_library_ident); } else if (e->kind == Entity_Variable) { @@ -1129,24 +1259,7 @@ gb_internal void build_global_groups(Checker *c, GlobalGroupGraph *g) { } i32 node_count = cast(i32)g->nodes.count; - array_init(&g->offsets, heap_allocator(), node_count+1); - array_init(&g->targets, heap_allocator(), edge_to.count); - for (i32 v = 0; v <= node_count; v++) { - g->offsets[v] = 0; - } - for (i32 from : edge_from) { - g->offsets[from+1] += 1; - } - for (i32 v = 0; v < node_count; v++) { - g->offsets[v+1] += g->offsets[v]; - } - { - auto fill = array_clone(heap_allocator(), g->offsets); - defer (array_free(&fill)); - for (isize i = 0; i < edge_from.count; i++) { - g->targets[fill[edge_from[i]]++] = edge_to[i]; - } - } + global_graph_csr(node_count, edge_from, edge_to, &g->offsets, &g->targets); array_init(&g->group_of, heap_allocator(), node_count); i32 group_count = global_graph_scc(node_count, g->offsets, g->targets, &g->group_of); @@ -1350,7 +1463,609 @@ gb_internal void check_all_global_entities(Checker *c) { } -// -internal-global-entity-graph: the groups, weighted by the measured self time of their entities +// NOTE(bill, 2026-10-01) +// +// Cycles of global 'when's: a 'when' whose condition may need what its own branch declares, +// directly or through other 'when's. Found from syntax before anything is resolved: the nodes +// are the 'when's and 'foreign' blocks, the declarations in their branches, and the global +// entities their conditions reach. +// +// Each cycle is decided by trying every choice of its branches; exactly one choice must be consistent + +enum GlobalWhenNodeKind : u8 { + GlobalWhenNode_Source, + GlobalWhenNode_Decl, + GlobalWhenNode_Entity, +}; + +struct GlobalWhenNode { + GlobalWhenNodeKind kind; + GlobalDeclSource * source; // of a source or a declaration + Ast * decl; + Entity * entity; +}; + +gb_internal void find_global_when_cycles(void) { + auto nodes = array_make (heap_allocator(), 0, global_decl_sources.count); + auto edge_from = array_make (heap_allocator(), 0, global_decl_sources.count); + auto edge_to = array_make (heap_allocator(), 0, global_decl_sources.count); + auto refs = array_make (heap_allocator(), 0, 64); + auto hits = array_make(heap_allocator(), 0, 16); + defer (array_free(&nodes)); + defer (array_free(&edge_from)); + defer (array_free(&edge_to)); + defer (array_free(&refs)); + defer (array_free(&hits)); + + PtrMap node_of = {}; + map_init(&node_of, 2*global_decl_sources.count); + defer (map_destroy(&node_of)); + + auto add_node = [&](void *key, GlobalWhenNode const &node) -> i32 { + i32 *found = map_get(&node_of, key); + if (found != nullptr) { + return *found; + } + i32 v = cast(i32)nodes.count; + map_set(&node_of, key, v); + array_add(&nodes, node); + return v; + }; + auto add_edge = [&](i32 from, i32 to) { + array_add(&edge_from, from); + array_add(&edge_to, to); + }; + + for (GlobalDeclSource *src : global_decl_sources) { + add_node(src, GlobalWhenNode{GlobalWhenNode_Source, src}); + } + + GlobalGraphWalk w = {}; + w.refs = &refs; + w.hits = &hits; + for (i32 v = 0; v < nodes.count; v++) { + GlobalWhenNode node = nodes[v]; + array_clear(&refs); + array_clear(&hits); + switch (node.kind) { + case GlobalWhenNode_Source: + w.scope = node.source->file->scope; + if (node.source->node->kind == Ast_WhenStmt) { + global_graph_walk(&w, node.source->node->WhenStmt.cond); + } else { + global_graph_walk_attribute_values(&w, node.source->node->ForeignBlockDecl.attributes); + } + if (node.source->parent != nullptr) { + add_edge(v, *map_get(&node_of, cast(void *)node.source->parent)); + } + break; + case GlobalWhenNode_Decl: + w.scope = node.source->file->scope; + if (node.decl->kind == Ast_ValueDecl) { + global_graph_walk(&w, node.decl->ValueDecl.type); + global_graph_walk_slice(&w, node.decl->ValueDecl.values); + global_graph_walk_attribute_values(&w, node.decl->ValueDecl.attributes); + } else if (node.decl->kind == Ast_ForeignImportDecl) { + global_graph_walk_attribute_values(&w, node.decl->ForeignImportDecl.attributes); + } + add_edge(v, *map_get(&node_of, cast(void *)node.source)); + break; + case GlobalWhenNode_Entity: + global_graph_walk_entity(&w, node.entity, node.entity->decl_info); + break; + } + + for (Entity *e : refs) { + if (e->decl_info != nullptr && e->scope != nullptr && (e->scope->flags & ScopeFlag_File) != 0) { + add_edge(v, add_node(e, GlobalWhenNode{GlobalWhenNode_Entity, nullptr, nullptr, e})); + } + } + + // NOTE(bill):: a name that may be declared by a 'when' depends on its declarations there, and on that 'when' + for (GlobalPlaceholderHit const &hit : hits) { + PtrMap *m = hit.scope->placeholders; + for (auto *entry = multi_map_find_first(m, cast(u64)hit.name.value); entry != nullptr; entry = multi_map_find_next(m, entry)) { + GlobalDeclSource *src = entry->value; + for (GlobalDeclSourceName const &n : src->names) { + if (n.name == hit.name && n.scope == hit.scope) { + add_edge(v, add_node(n.decl, GlobalWhenNode{GlobalWhenNode_Decl, src, n.decl})); + } + } + } + } + } + + i32 node_count = cast(i32)nodes.count; + Array offsets = {}; + Array targets = {}; + defer (array_free(&offsets)); + defer (array_free(&targets)); + + global_graph_csr(node_count, edge_from, edge_to, &offsets, &targets); + auto comp_of = array_make(heap_allocator(), node_count); + defer (array_free(&comp_of)); + + i32 comp_count = global_graph_scc(node_count, offsets, targets, &comp_of); + + auto comp_size = array_make (heap_allocator(), comp_count); + auto comp_cycle = array_make(heap_allocator(), comp_count); + defer (array_free(&comp_size)); + defer (array_free(&comp_cycle)); + + for (i32 ci = 0; ci < comp_count; ci++) { + comp_size[ci] = 0; + comp_cycle[ci] = nullptr; + } + for (i32 v = 0; v < node_count; v++) { + comp_size[comp_of[v]] += 1; + } + auto is_cyclic = [&](i32 v) -> bool { + if (comp_size[comp_of[v]] > 1) { + return true; + } + for (i32 i = offsets[v]; i < offsets[v+1]; i++) { + if (targets[i] == v) { + return true; + } + } + return false; + }; + + // NOTE: sources are the first nodes, in source order + for (i32 v = 0; v < global_decl_sources.count; v++) { + if (!is_cyclic(v)) { + continue; + } + GlobalWhenCycle *&cycle = comp_cycle[comp_of[v]]; + if (cycle == nullptr) { + cycle = permanent_alloc_item(); + array_init(&cycle->sources, heap_allocator()); + ptr_set_init(&cycle->decls); + global_when_cycle_count += 1; + } + GlobalDeclSource *src = nodes[v].source; + src->cycle = cycle; + src->cycle_index = cast(i32)cycle->sources.count; + array_add(&cycle->sources, src); + global_when_cycle_sources += 1; + } + for (i32 v = 0; v < node_count; v++) { + GlobalWhenCycle *cycle = comp_cycle[comp_of[v]]; + if (cycle == nullptr) { + continue; + } + if (nodes[v].kind == GlobalWhenNode_Decl) { + ptr_set_add(&cycle->decls, nodes[v].decl); + } else if (nodes[v].kind == GlobalWhenNode_Entity && nodes[v].entity->decl_info->decl_node != nullptr) { + ptr_set_add(&cycle->decls, nodes[v].entity->decl_info->decl_node); + } + } +} + +gb_internal ForeignContext global_decl_source_foreign_context(GlobalDeclSource *src) { + for (GlobalDeclSource *p = src; p != nullptr; p = p->parent) { + if (p->node->kind == Ast_ForeignBlockDecl) { + return p->foreign_context; + } + } + return {}; +} + +// Scratch entities for a declaration, made as `check_collect_value_decl` would but put nowhere +gb_internal Entity *global_when_trial_entity(GlobalWhenTrial *t, Ast *decl, AstFile *file, ForeignContext const &foreign_context, InternedString name) { + Array **found = map_get(&t->decl_entities, decl); + Array *entities = found ? *found : nullptr; + if (entities == nullptr) { + entities = gb_alloc_item(heap_allocator(), Array); + array_init(entities, heap_allocator()); + map_set(&t->decl_entities, decl, entities); + if (decl->kind != Ast_ValueDecl) { + t->unsupported = true; + return nullptr; + } + CheckerContext ctx = {}; + init_checker_context(&ctx, global_checker_ptr.load(std::memory_order_relaxed)); + UntypedExprInfoMap untyped = {}; + reset_checker_context(&ctx, file, &untyped); + ctx.decl = make_decl_info(file->scope, nullptr); // not a child of the package's + ctx.foreign_context = foreign_context; + ctx.trial_entities = entities; + Ast *clone = clone_ast(decl); + clone->state_flags &= ~StateFlag_BeenHandled; + check_collect_value_decl(&ctx, clone); + map_destroy(&untyped); + destroy_checker_context(&ctx); + for (Entity *e : *entities) { + ptr_set_add(&t->scratch, e); + } + } + for (Entity *e : *entities) { + if (entity_interned_name(e) == name) { + return e; + } + } + t->unsupported = true; + return nullptr; +} + +// Every scope lookup during a trial: `found` is what the scope itself holds +gb_internal Entity *global_when_trial_lookup(Scope *s, InternedString name, u32 hash, Entity *found) { + GlobalWhenTrial *t = global_when_trial; + GlobalWhenCycle *cycle = t->cycle; + if (found != nullptr) { + DeclInfo *d = found->decl_info; + if (d == nullptr || d->decl_node == nullptr || !ptr_set_exists(&cycle->decls, d->decl_node) || ptr_set_exists(&t->scratch, found)) { + return found; + } + if (t->real_depth > 0) { + t->broken = true; + return found; + } + if ((found->kind == Entity_Procedure && found->Procedure.is_foreign) || + (found->kind == Entity_Variable && found->Variable.is_foreign)) { + t->unsupported = true; + return found; + } + Entity *copy = global_when_trial_entity(t, d->decl_node, found->file, {}, name); + return copy != nullptr ? copy : found; + } + if (s->placeholders == nullptr) { + return nullptr; + } + PtrMap *m = s->placeholders; + for (auto *entry = multi_map_find_first(m, cast(u64)name.value); + entry != nullptr; + entry = multi_map_find_next(m, entry)) { + GlobalDeclSource *src = entry->value; + if (src->cycle != cycle) { + continue; + } + if (t->real_depth > 0) { + t->broken = true; + return nullptr; + } + u32 bit = 1u << src->cycle_index; + if ((t->reachable & bit) == 0) { + continue; + } + bool in_else = (t->mask & bit) == 0; + for (GlobalDeclSourceName const &n : src->names) { + if (n.name == name && n.scope == s && n.in_else == in_else) { + // NOTE(bill): the branches around it are needed too + for (GlobalDeclSource *p = src; p != nullptr && p->cycle == cycle; p = p->parent) { + t->used |= 1u << p->cycle_index; + } + return global_when_trial_entity(t, n.decl, src->file, global_decl_source_foreign_context(src), name); + } + } + } + return nullptr; +} + +// An entity outside the trial is checked for real, with errors shown; a scratch procedure is not checked, +// as its body would be queued +gb_internal bool global_when_trial_begin_entity(Entity *e, GlobalWhenTrialEntityScope *scope) { + GlobalWhenTrial *t = global_when_trial; + *scope = {}; + if (ptr_set_exists(&t->scratch, e)) { + if (e->kind == Entity_Procedure || e->kind == Entity_AsmTemplate) { + t->unsupported = true; + e->type = t_invalid; + e->state = EntityState_Resolved; + return false; + } + return true; + } + scope->trial = t; + scope->mute_depth = global_error_mute_depth; + global_error_mute_depth = 0; + t->real_depth += 1; + return true; +} + +gb_internal void global_when_trial_end_entity(GlobalWhenTrialEntityScope *scope) { + if (scope->trial != nullptr) { + scope->trial->real_depth -= 1; + global_error_mute_depth = scope->mute_depth; + } +} + +enum GlobalWhenFailureKind : u8 { + GlobalWhenFailure_None, + GlobalWhenFailure_Invalid, // Cannot be evaluated + GlobalWhenFailure_Disagrees, // Picks the other branch + GlobalWhenFailure_OwnBranch, // A chosen branch is needed to decide its own condition +}; + +struct GlobalWhenTrialResult { + GlobalWhenFailureKind failure; + i32 source; + bool value; + bool unsupported; + bool broken; +}; + +gb_internal GlobalWhenTrialResult try_global_when_choice(GlobalWhenCycle *cycle, u32 mask, u32 reachable) { + GlobalWhenTrialResult res = {}; + i32 k = cast(i32)cycle->sources.count; + u32 used[32] = {}; + + for (i32 i = 0; i < k; i++) { + if ((reachable & (1u<sources[i]; + ast_node(ws, WhenStmt, src->node); + + GlobalWhenTrial t = {}; + t.cycle = cycle; + t.mask = mask; + t.reachable = reachable; + + map_init(&t.decl_entities); + defer ({ + for (auto const &entry : t.decl_entities) { + array_free(entry.value); + gb_free(heap_allocator(), entry.value); + } + map_destroy(&t.decl_entities); + }); + + ptr_set_init(&t.scratch); + defer (ptr_set_destroy(&t.scratch)); + + CheckerContext ctx = {}; + init_checker_context(&ctx, global_checker_ptr.load(std::memory_order_relaxed)); + defer (destroy_checker_context(&ctx)); + + UntypedExprInfoMap untyped = {}; + defer (map_destroy(&untyped)); + + + reset_checker_context(&ctx, src->file, &untyped); + // so no dependency is recorded + ctx.decl = make_decl_info(src->file->scope, nullptr); + ctx.foreign_context = global_decl_source_foreign_context(src); + + GlobalWhenTrial *prev = global_when_trial; + global_when_trial = &t; + + Ast *cond = clone_ast(ws->cond); + i64 muted = error_mute_count(); + begin_error_mute(); + + Operand o = {}; + check_expr(&ctx, &o, cond); + end_error_mute(); + global_when_trial = prev; + global_when_trial_count += 1; + + bool ok = error_mute_count() == muted && o.mode == Addressing_Constant && o.value.kind == ExactValue_Bool; + used[i] = t.used; + res.unsupported = t.unsupported; + res.broken = t.broken; + + if (res.unsupported || res.broken) { + return res; + } + if (!ok || o.value.value_bool != ((mask & (1u<sources.count; i++) { + GlobalDeclSource *src = cycle->sources[i]; + GlobalDeclSource *p = src->parent; + bool r = (unreachable & (1u<cycle == cycle) { + u32 pb = 1u << p->cycle_index; + r = (reachable & pb) != 0; + if (p->node->kind == Ast_WhenStmt) { + r = r && ((mask & pb) != 0) != src->in_else; + } + } + if (r) { + reachable |= 1u << i; + } + } + return reachable; +} + +gb_internal gbString global_when_choice_string(gbString s, GlobalWhenCycle *cycle, u32 mask, u32 reachable) { + for (i32 i = 0; i < cycle->sources.count; i++) { + u32 bit = 1u << i; + s = gb_string_append_fmt(s, "%s%s", i > 0 ? ", " : "", (reachable & bit) == 0 ? "unreachable" : (mask & bit) ? "taken" : "not taken"); + } + return s; +} + +gb_internal void error_line_global_when_sources(GlobalWhenCycle *cycle) { + for (GlobalDeclSource *src : cycle->sources) { + gbString cond = expr_to_string(src->node->WhenStmt.cond); + error_line("\t'when' at %s: %s\n", token_pos_to_string(src->node->WhenStmt.token.pos), cond); + gb_string_free(cond); + } +} + +gb_internal void commit_global_when_cycle(GlobalWhenCycle *cycle, u32 mask, bool check_conditions) { + for (GlobalDeclSource *src : cycle->sources) { + src->predetermined = true; + src->predetermined_cond = (mask & (1u << src->cycle_index)) != 0; + } + for (GlobalDeclSource *src : cycle->sources) { + resolve_global_decl_source(src, {}); + } + if (!check_conditions) { + return; + } + // NOTE: for real, now that every chosen branch is collected, which must give the same values + for (GlobalDeclSource *src : cycle->sources) { + if (!src->reachable) { + continue; + } + ast_node(ws, WhenStmt, src->node); + + CheckerContext ctx = {}; + init_checker_context(&ctx, global_checker_ptr.load(std::memory_order_relaxed)); + defer (destroy_checker_context(&ctx)); + + UntypedExprInfoMap untyped = {}; + reset_checker_context(&ctx, src->file, &untyped); + defer (map_destroy(&untyped)); + + ctx.foreign_context = global_decl_source_foreign_context(src); + + Operand o = {}; + check_expr(&ctx, &o, ws->cond); + if (o.mode != Addressing_Constant || o.value.kind != ExactValue_Bool || o.value.value_bool != ws->determined_cond) { + error(ws->token, "Internal compiler error: this global 'when' changed its branch after its cycle was decided"); + } + add_untyped_expressions(ctx.info, &untyped); + } +} + +gb_internal void search_global_when_cycle(GlobalWhenCycle *cycle) { + cycle->searched = true; + i32 k = cast(i32)cycle->sources.count; + for (GlobalDeclSource *src : cycle->sources) { + if (src->node->kind != Ast_WhenStmt) { + // NOTE(bill): resolved on demand where a cycle is reported as such + return; + } + } + Token token = cycle->sources[0]->node->WhenStmt.token; + + i32 const MAX_SOURCES = 8; // 2^8 == 256 combinations + if (k > MAX_SOURCES) { + ERROR_BLOCK(); + error(token, "Too many combinations of global 'when' branches: %d 'when's depend on each other, which gives 2^%d combinations, more than %d", + k, k, 1 << MAX_SOURCES); + error_line_global_when_sources(cycle); + return; + } + + // The parents outside the cycle are decided for real first + u32 unreachable = 0; + for (GlobalDeclSource *src : cycle->sources) { + GlobalDeclSource *p = src->parent; + if (p == nullptr || p->cycle == cycle) { + continue; + } + resolve_global_decl_source(p, {}); + bool r = p->state == EntityState_Resolved && p->reachable; + if (p->node->kind == Ast_WhenStmt) { + r = r && p->node->WhenStmt.determined_cond != src->in_else; + } + if (!r) { + unreachable |= 1u << src->cycle_index; + } + } + + auto consistent = array_make(heap_allocator()); + auto failures = array_make(heap_allocator()); + auto failed = array_make(heap_allocator()); + defer (array_free(&consistent)); + defer (array_free(&failures)); + defer (array_free(&failed)); + + for (u32 mask = 0; mask < (1u << k); mask++) { + u32 reachable = global_when_cycle_reachable(cycle, mask, unreachable); + if (mask & ~reachable) { + // An unreachable 'when' is only counted as not taken + continue; + } + GlobalWhenTrialResult result = try_global_when_choice(cycle, mask, reachable); + if (result.broken) { + error(token, "Internal compiler error: deciding this cycle of global 'when's checked a declaration that depends on it"); + return; + } + if (result.unsupported) { + // Needs a procedure or library declared in the cycle, so it is resolved on demand instead + return; + } + if (result.failure == GlobalWhenFailure_None) { + array_add(&consistent, mask); + } else { + array_add(&failures, result); + array_add(&failed, mask); + } + } + + if (consistent.count == 1) { + commit_global_when_cycle(cycle, consistent[0], true); + return; + } + + ERROR_BLOCK(); + if (consistent.count == 0) { + error(token, "Contradictory global 'when' conditions: no choice of their branches is consistent"); + error_line_global_when_sources(cycle); + for (isize i = 0; i < failed.count && i < 16; i++) { + GlobalWhenTrialResult const &r = failures[i]; + + u32 reachable = global_when_cycle_reachable(cycle, failed[i], unreachable); + + gbString s = global_when_choice_string(gb_string_make(heap_allocator(), ""), cycle, failed[i], reachable); + defer (gb_string_free(s)); + + TokenPos pos = cycle->sources[r.source]->node->WhenStmt.token.pos; + switch (r.failure) { + case GlobalWhenFailure_Invalid: + error_line("\t[%s]: the 'when' at %s cannot be evaluated\n", s, token_pos_to_string(pos)); + break; + case GlobalWhenFailure_Disagrees: + error_line("\t[%s]: the 'when' at %s evaluates to %s\n", s, token_pos_to_string(pos), r.value ? "true" : "false"); + break; + case GlobalWhenFailure_OwnBranch: + error_line("\t[%s]: a taken branch is needed to decide its own condition\n", s); + break; + } + } + commit_global_when_cycle(cycle, 0, false); + } else { + error(token, "Ambiguous global 'when' conditions: %td choices of their branches are consistent", consistent.count); + error_line_global_when_sources(cycle); + for (isize i = 0; i < consistent.count; i++) { + u32 reachable = global_when_cycle_reachable(cycle, consistent[i], unreachable); + gbString s = global_when_choice_string(gb_string_make(heap_allocator(), ""), cycle, consistent[i], reachable); + defer (gb_string_free(s)); + + error_line("\tchoice %td: %s\n", i+1, s); + } + commit_global_when_cycle(cycle, consistent[0], false); + } +} + + +// `-internal-global-entity-graph` +// The groups weighted by the measured self time of their entities struct GlobalGraphSortItem { u64 key; @@ -1410,11 +2125,11 @@ gb_internal void print_global_groups(GlobalGroupGraph *g) { defer (array_free(&pkg_entities)); defer (map_destroy(&pkg_index)); - u64 total_ticks = 0; - u64 when_ticks = 0; - isize untimed = 0; - i32 largest = -1; - isize cyclic = 0; + u64 total_ticks = 0; + u64 when_ticks = 0; + isize untimed = 0; + i32 largest = -1; + isize cyclic = 0; mutex_lock(&global_entity_time_mutex); for (auto const &entry : global_entity_times) { @@ -1451,7 +2166,7 @@ gb_internal void print_global_groups(GlobalGroupGraph *g) { } mutex_unlock(&global_entity_time_mutex); - // NOTE: every dependency of a group has a lower index + // NOTE(bill): every dependency of a group has a lower index i32 critical = -1; for (i32 gi = 0; gi < group_count; gi++) { seen[gi] = -1; @@ -1488,6 +2203,7 @@ gb_internal void print_global_groups(GlobalGroupGraph *g) { gb_printf_err(" critical path: %.3f ms over %d groups -> at most %.2fx speedup\n", critical_ms, critical >= 0 ? len[critical] : 0, critical_ms > 0 ? total_ms/critical_ms : 0.0); + gb_printf_err(" 'when' cycles: %td (%td 'when's), %td conditions tried\n", global_when_cycle_count, global_when_cycle_sources, global_when_trial_count); gb_printf_err(" check_import_entities (sequential, includes entity checks it triggers):\n"); for (isize i = 0; i < GlobalImportStage_COUNT; i++) { gb_printf_err(" %10.3f ms %s\n", global_graph_ms(global_import_stage_ticks[i], freq), global_import_stage_names[i]); diff --git a/tests/issues/run.bat b/tests/issues/run.bat index 991a87d7f..b5b90339c 100644 --- a/tests/issues/run.bat +++ b/tests/issues/run.bat @@ -54,7 +54,9 @@ set COMMON=-define:ODIN_TEST_FANCY=false -file -vet -strict-style -ignore-unused ..\..\..\odin check ..\test_issue_ambiguous_union_literal.odin %COMMON% 2>&1 | find /c "Error:" | findstr /x "1" || exit /b ..\..\..\odin test ..\test_issue_proc_constant_instantiation.odin %COMMON% || exit /b ..\..\..\odin test ..\test_issue_global_when_order.odin %COMMON% || exit /b -..\..\..\odin check ..\test_issue_global_when_cycle.odin -no-entry-point %COMMON% 2>&1 | find /c "Cyclic dependency between global" | findstr /x "3" || exit /b +..\..\..\odin check ..\test_issue_global_when_cycle.odin -no-entry-point %COMMON% 2>&1 | find /c "Contradictory global" | findstr /x "4" || exit /b +..\..\..\odin test ..\test_issue_global_when_cycle_accepted.odin %COMMON% || exit /b +..\..\..\odin check ..\test_issue_global_when_cycle_ambiguous.odin -no-entry-point %COMMON% 2>&1 | find /c "Ambiguous global" | findstr /x "1" || exit /b ..\..\..\odin check ..\test_issue_global_when_shadowing.odin -no-entry-point %COMMON% 2>&1 | find /c "within a global" | findstr /x "2" || exit /b ..\..\..\odin check ..\test_issue_7336.odin -no-entry-point %COMMON% || exit /b ..\..\..\odin check ..\test_issue_ellipsis_type_call.odin -no-entry-point %COMMON% 2>&1 | find /c "Error:" | findstr /x "10" || exit /b diff --git a/tests/issues/run.sh b/tests/issues/run.sh index 41f108d4d..8bc063be0 100755 --- a/tests/issues/run.sh +++ b/tests/issues/run.sh @@ -130,7 +130,14 @@ fi $ODIN test ../test_issue_proc_constant_instantiation.odin $COMMON $ODIN test ../test_issue_swizzle_multi_assign.odin $COMMON $ODIN test ../test_issue_global_when_order.odin $COMMON -if [[ $($ODIN check ../test_issue_global_when_cycle.odin -no-entry-point $COMMON_CHECK 2>&1 >/dev/null | grep -c "Cyclic dependency between global") -eq 3 ]]; then +if [[ $($ODIN check ../test_issue_global_when_cycle.odin -no-entry-point $COMMON_CHECK 2>&1 >/dev/null | grep -c "Contradictory global 'when'") -eq 4 ]]; then + echo "SUCCESSFUL 1/1" +else + echo "SUCCESSFUL 0/1" + exit 1 +fi +$ODIN test ../test_issue_global_when_cycle_accepted.odin $COMMON +if [[ $($ODIN check ../test_issue_global_when_cycle_ambiguous.odin -no-entry-point $COMMON_CHECK 2>&1 >/dev/null | grep -c "Ambiguous global 'when'") -eq 1 ]]; then echo "SUCCESSFUL 1/1" else echo "SUCCESSFUL 0/1" diff --git a/tests/issues/test_issue_global_when_cycle.odin b/tests/issues/test_issue_global_when_cycle.odin index 77f83a028..3472c9331 100644 --- a/tests/issues/test_issue_global_when_cycle.odin +++ b/tests/issues/test_issue_global_when_cycle.odin @@ -1,10 +1,14 @@ -// Each of these global 'when's needs a name it may declare itself, so all three are cycles +// Cycles of global 'when's where no choice of branches is consistent, so each is an error package test_issues -// each 'when' needs a name the other may declare +// taken, `int` becomes 4 bytes and the first condition is false; not taken, the second is true when size_of(int) == 8 { A :: 1 } when A == 1 { int :: i32 } +// both taken is consistent, but each branch is needed to decide its own condition +when Y == 1 { X :: 1 } +when X == 1 { Y :: 1 } + // a 'when' needing a name it may declare when B == 1 { B :: 1 } From 94bb9558716636a327ed398b0944b7153d613181 Mon Sep 17 00:00:00 2001 From: gingerBill Date: Thu, 1 Oct 2026 19:05:15 +0100 Subject: [PATCH 08/17] Fix #7566: name polymorphic instances by the values of compound constant parameters and not their spelling --- src/name_canonicalization.cpp | 59 +++++++++++++++++++++++++++++++++-- src/name_canonicalization.hpp | 1 + 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/src/name_canonicalization.cpp b/src/name_canonicalization.cpp index 0a4f60fec..12b1f23cf 100644 --- a/src/name_canonicalization.cpp +++ b/src/name_canonicalization.cpp @@ -510,9 +510,7 @@ gb_internal void write_canonical_params(TypeWriter *w, Type *params) { } } } - gbString s = exact_value_to_string(v->Constant.value, 1<<16); - type_writer_append(w, s, gb_string_length(s)); - gb_string_free(s); + write_canonical_exact_value(w, v->Constant.value); } break; default: @@ -523,6 +521,61 @@ gb_internal void write_canonical_params(TypeWriter *w, Type *params) { return; } +gb_internal void write_canonical_exact_value(TypeWriter *w, ExactValue const &v); + +gb_internal void write_canonical_constant_expr(TypeWriter *w, Ast *expr) { + if (expr->tav.mode == Addressing_Constant) { + write_canonical_exact_value(w, expr->tav.value); + return; + } + gbString s = write_expr_to_string(gb_string_make(heap_allocator(), ""), expr, false); + type_writer_append(w, s, gb_string_length(s)); + gb_string_free(s); +} + +gb_internal void write_canonical_exact_value(TypeWriter *w, ExactValue const &v) { + if (v.kind == ExactValue_Compound && v.value_compound != nullptr && v.value_compound->kind == Ast_CompoundLit) { + ast_node(cl, CompoundLit, v.value_compound); + type_writer_appendb(w, '{'); + for_array(i, cl->elems) { + if (i > 0) { + type_writer_appendc(w, CANONICAL_FIELD_SEPARATOR); + } + Ast *elem = cl->elems[i]; + if (elem->kind == Ast_FieldValue) { + Ast *field = elem->FieldValue.field; + if (field->kind == Ast_Ident) { + type_writer_append(w, field->Ident.token.string.text, field->Ident.token.string.len); + } else if (is_ast_range(field)) { + write_canonical_constant_expr(w, field->BinaryExpr.left); + type_writer_append(w, field->BinaryExpr.op.string.text, field->BinaryExpr.op.string.len); + write_canonical_constant_expr(w, field->BinaryExpr.right); + } else { + write_canonical_constant_expr(w, field); + } + type_writer_appendc(w, "="); + elem = elem->FieldValue.value; + } + write_canonical_constant_expr(w, elem); + } + type_writer_appendb(w, '}'); + return; + } + if (v.kind == ExactValue_Variant && v.value_variant != nullptr) { + Ast *expr = v.value_variant; + bool is_self = expr->tav.value.kind == ExactValue_Variant && expr->tav.value.value_variant == expr; + if (expr->tav.mode == Addressing_Constant && !is_self) { + write_type_to_canonical_string(w, expr->tav.type); + type_writer_appendc(w, "="); + write_canonical_exact_value(w, expr->tav.value); + return; + } + } + gbString s = exact_value_to_string(v, 1<<16); + type_writer_append(w, s, gb_string_length(s)); + gb_string_free(s); +} + gb_internal u64 type_hash_canonical_type(Type *type) { if (type == nullptr) { return 0; diff --git a/src/name_canonicalization.hpp b/src/name_canonicalization.hpp index 00b450fbe..cec2b99d3 100644 --- a/src/name_canonicalization.hpp +++ b/src/name_canonicalization.hpp @@ -46,6 +46,7 @@ struct TypeWriter; gb_internal void write_type_to_canonical_string(TypeWriter *w, Type *type); +gb_internal void write_canonical_exact_value(TypeWriter *w, ExactValue const &v); gb_internal void write_canonical_entity_name(TypeWriter *w, Entity *e); gb_internal u64 type_hash_canonical_type(Type *type); gb_internal String type_to_canonical_string(gbAllocator allocator, Type *type); From 73c3a96de1588cb532ac4fbf80323aefad67cd3e Mon Sep 17 00:00:00 2001 From: gingerBill Date: Thu, 1 Oct 2026 21:07:41 +0100 Subject: [PATCH 09/17] Make global checking state thread-safe: read-only global scopes, per-group untyped and #soa, deferred link-name checks, fresh contexts for on-demand globals --- src/check_decl.cpp | 204 +++++++++++++++++++++++++---------------- src/check_expr.cpp | 29 +++--- src/check_stmt.cpp | 18 +--- src/check_type.cpp | 69 +++++++------- src/checker.cpp | 131 +++++++++++++++++--------- src/checker.hpp | 36 +++++++- src/checker_global.cpp | 30 +++++- src/entity.cpp | 4 + src/llvm_backend.cpp | 6 ++ src/types.cpp | 70 +++++++++++--- 10 files changed, 396 insertions(+), 201 deletions(-) diff --git a/src/check_decl.cpp b/src/check_decl.cpp index 3343465ae..4a40ba7a4 100644 --- a/src/check_decl.cpp +++ b/src/check_decl.cpp @@ -170,7 +170,7 @@ gb_internal void override_entity_in_scope(Entity *original_entity, Entity *new_e // has been "evaluated" and the variant data can be copied across rw_mutex_lock(&found_scope->mutex); - scope_map_insert(&found_scope->elements, original_intern, hash, new_entity); + scope_map_replace(&found_scope->elements, original_intern, hash, new_entity); rw_mutex_unlock(&found_scope->mutex); original_entity->flags |= EntityFlag_Overridden; @@ -1257,41 +1257,95 @@ gb_internal void check_target_feature_attributes(AttributeContext &ac, Entity *e } } -gb_internal void check_foreign_procedure(CheckerContext *ctx, Entity *e, DeclInfo *d) { - GB_ASSERT(e != nullptr); - GB_ASSERT(e->kind == Entity_Procedure); - String name = e->Procedure.link_name; +gb_internal void add_link_name_use(CheckerInfo *info, String name, Entity *e, DeclInfo *d, LinkNameUseKind kind) { + mutex_lock(&info->foreign_mutex); + array_add(&info->link_names, LinkNameUse{name, e, d, kind}); + mutex_unlock(&info->foreign_mutex); +} - mutex_lock(&ctx->info->foreign_mutex); +gb_internal GB_COMPARE_PROC(link_name_use_cmp) { + LinkNameUse const *x = cast(LinkNameUse const *)a; + LinkNameUse const *y = cast(LinkNameUse const *)b; + i32 cmp = string_compare(x->name, y->name); + if (cmp != 0) { + return cmp; + } + if (x->entity != y->entity) { + return entity_source_order_cmp(x->entity, y->entity); + } + return i32_cmp(x->kind, y->kind); +} +gb_internal void check_link_name_uses(Checker *c) { + auto &uses = c->info.link_names; + array_sort(uses, link_name_use_cmp); + + LinkNameUse *first = nullptr; + for (isize i = 0; i < uses.count; i++) { + LinkNameUse *u = &uses[i]; + if (i > 0 && uses[i-1].name != u->name) { + first = nullptr; + } else if (i > 0 && uses[i-1].entity == u->entity) { + continue; + } + String name = u->name; + if (name == "main" && u->kind != LinkNameUse_Variable) { + if (u->kind == LinkNameUse_ForeignProcedure || u->entity->pkg->kind != Package_Runtime) { + error(u->decl->proc_lit, "The link name 'main' is reserved for internal use"); + } + continue; + } + if (first == nullptr) { + first = u; + continue; + } + + Entity *e = u->entity; + Entity *f = first->entity; - auto *fp = &ctx->info->foreigns; - StringHashKey key = string_hash_string(name); - Entity **found = string_map_get(fp, key); - if (found && e != *found) { - Entity *f = *found; TokenPos pos = f->token.pos; + Type *this_type = base_type(e->type); Type *other_type = base_type(f->type); - if (is_type_proc(this_type) && is_type_proc(other_type)) { - if (!are_signatures_similar_enough(this_type, other_type)) { - error(d->proc_lit, - "Redeclaration of foreign procedure '%.*s' with different type signatures\n" + + switch (u->kind) { + case LinkNameUse_ForeignProcedure: + if (is_type_proc(this_type) && is_type_proc(other_type)) { + if (!are_signatures_similar_enough(this_type, other_type)) { + error(u->decl->proc_lit, + "Redeclaration of foreign procedure '%.*s' with different type signatures\n" + "\tat %s", + LIT(name), token_pos_to_string(pos)); + } + } else if (!signature_parameter_similar_enough(this_type, other_type)) { + error(u->decl->proc_lit, + "Foreign entity '%.*s' previously declared elsewhere with a different type\n" "\tat %s", LIT(name), token_pos_to_string(pos)); } - } else if (!signature_parameter_similar_enough(this_type, other_type)) { - error(d->proc_lit, - "Foreign entity '%.*s' previously declared elsewhere with a different type\n" - "\tat %s", + break; + case LinkNameUse_Procedure: + // TODO(bill): Better error message? + error(u->decl->proc_lit, + "Non-unique linking name for procedure '%.*s'\n" + "\tother at %s", LIT(name), token_pos_to_string(pos)); + break; + case LinkNameUse_Variable: + if (e->type == nullptr || f->type == nullptr || !signature_parameter_similar_enough(this_type, other_type)) { + error(e->token, + "Foreign entity '%.*s' previously declared elsewhere with a different type\n" + "\tat %s", + LIT(name), token_pos_to_string(pos)); + } + break; } - } else if (name == "main") { - error(d->proc_lit, "The link name 'main' is reserved for internal use"); - } else { - string_map_set(fp, key, e); } +} - mutex_unlock(&ctx->info->foreign_mutex); +gb_internal void check_foreign_procedure(CheckerContext *ctx, Entity *e, DeclInfo *d) { + GB_ASSERT(e != nullptr); + GB_ASSERT(e->kind == Entity_Procedure); + add_link_name_use(ctx->info, e->Procedure.link_name, e, d, LinkNameUse_ForeignProcedure); } gb_internal void check_proc_decl(CheckerContext *ctx, Entity *e, DeclInfo *d) { @@ -1562,14 +1616,17 @@ gb_internal void check_proc_decl(CheckerContext *ctx, Entity *e, DeclInfo *d) { } if (e->pkg->kind == Package_Init) { - if (ctx->info->entry_point != nullptr) { + mutex_lock(&ctx->info->entry_point_mutex); + Entity *prev_entry_point = ctx->info->entry_point; + if (prev_entry_point == nullptr) { + ctx->info->entry_point = e; + } + mutex_unlock(&ctx->info->entry_point_mutex); + if (prev_entry_point != nullptr) { begin_error_block(); error(e->token, "Redeclaration of the entry point procedure 'main'"); error_line("\tSuggestion: Is this a single-file package? If so, try compiling using the `-file` flag.\n"); end_error_block(); - - } else { - ctx->info->entry_point = e; } } } @@ -1660,28 +1717,7 @@ gb_internal void check_proc_decl(CheckerContext *ctx, Entity *e, DeclInfo *d) { name = e->Procedure.link_name; } if (e->Procedure.link_name.len > 0 || is_export) { - mutex_lock(&ctx->info->foreign_mutex); - - auto *fp = &ctx->info->foreigns; - StringHashKey key = string_hash_string(name); - Entity **found = string_map_get(fp, key); - if (found) { - Entity *f = *found; - TokenPos pos = f->token.pos; - // TODO(bill): Better error message? - error(d->proc_lit, - "Non unique linking name for procedure '%.*s'\n" - "\tother at %s", - LIT(name), token_pos_to_string(pos)); - } else if (name == "main") { - if (d->entity.load()->pkg->kind != Package_Runtime) { - error(d->proc_lit, "The link name 'main' is reserved for internal use"); - } - } else { - string_map_set(fp, key, e); - } - - mutex_unlock(&ctx->info->foreign_mutex); + add_link_name_use(ctx->info, name, e, d, LinkNameUse_Procedure); } } @@ -1777,24 +1813,7 @@ gb_internal void check_global_variable_decl(CheckerContext *ctx, Entity *e, Ast name = e->Variable.link_name; } - auto *fp = &ctx->info->foreigns; - StringHashKey key = string_hash_string(name); - Entity **found = string_map_get(fp, key); - if (found) { - Entity *f = *found; - TokenPos pos = f->token.pos; - Type *this_type = base_type(e->type); - Type *other_type = base_type(f->type); - bool type_is_null = (e->type == nullptr || f->type == nullptr); - if (type_is_null || !signature_parameter_similar_enough(this_type, other_type)) { - error(e->token, - "Foreign entity '%.*s' previously declared elsewhere with a different type\n" - "\tat %s", - LIT(name), token_pos_to_string(pos)); - } - } else { - string_map_set(fp, key, e); - } + add_link_name_use(ctx->info, name, e, decl, LinkNameUse_Variable); } if (e->Variable.link_name.len > 0) { @@ -1910,9 +1929,15 @@ gb_internal void check_proc_group_decl(CheckerContext *ctx, Entity *pg_entity, D ptr_set_destroy(&entity_set); + // NOTE(bill, 2026-10-01): an invalid overload is dropped from this group only, as others may be using that procedure + auto invalid = array_make(temporary_allocator(), pge->entities.count); + for (isize j = 0; j < pge->entities.count; j++) { + invalid[j] = false; + } + for (isize j = 0; j < pge->entities.count; j++) { Entity *p = pge->entities[j]; - if (p->type == t_invalid) { + if (p->type == t_invalid || invalid[j]) { // NOTE(bill): This invalid overload has already been handled continue; } @@ -1931,7 +1956,7 @@ gb_internal void check_proc_group_decl(CheckerContext *ctx, Entity *pg_entity, D TokenPos pos = q->token.pos; - if (q->type == nullptr || q->type == t_invalid) { + if (q->type == nullptr || q->type == t_invalid || invalid[k]) { continue; } @@ -1987,10 +2012,17 @@ gb_internal void check_proc_group_decl(CheckerContext *ctx, Entity *pg_entity, D if (is_invalid) { error_line("\tprevious procedure at %s\n", token_pos_to_string(pos)); - q->type = t_invalid; + invalid[k] = true; } } } + isize valid_count = 0; + for (isize j = 0; j < pge->entities.count; j++) { + if (!invalid[j]) { + pge->entities[valid_count++] = pge->entities[j]; + } + } + pge->entities.count = valid_count; AttributeContext ac = {}; check_decl_attributes(ctx, d->attributes, proc_group_attribute, &ac); @@ -2153,6 +2185,7 @@ gb_internal void check_entity_decl(CheckerContext *ctx, Entity *e, DeclInfo *d, mutex_unlock(&ctx->info->lazy_mutex); return; } + lazy_mutex_depth += 1; } GlobalEntityTimingFrame timing_frame = global_entity_timing_begin(e); @@ -2174,18 +2207,34 @@ gb_internal void check_entity_decl(CheckerContext *ctx, Entity *e, DeclInfo *d, } CheckerContext c = *ctx; + if (d->scope->flags & ScopeFlag_File) { + // NOTE(bill): a global is checked in a context of its own file, never in that of whatever needed it first, + // which may be in another file or package, or a procedure body. + // Only the cycle detection carries over. + CheckerTypePath *type_path = c.type_path; + UntypedExprInfoMap *untyped = c.untyped; + gb_zero_size(&c.pkg, gb_size_of(CheckerContext) - gb_offset_of(CheckerContext, pkg)); + add_curr_ast_file(&c, d->scope->file); + c.type_path = type_path; + c.untyped = untyped; + } c.scope = d->scope; c.decl = d; c.type_level = 0; c.curr_proc_calling_convention = ProcCC_Contextless; + // NOTE: a file scope's is set from its own file and shared by every thread, see `create_scope_from_file` + bool set_context = (c.scope->flags & ScopeFlag_File) == 0; auto prev_flags = c.scope->flags; - defer (c.scope->flags = prev_flags); - - if (check_feature_flags(ctx, d->decl_node) & OptInFeatureFlag_GlobalContext) { - c.scope->flags |= ScopeFlag_ContextDefined; - } else { - c.scope->flags &= ~ScopeFlag_ContextDefined; + defer (if (set_context) { + c.scope->flags = prev_flags; + }); + if (set_context) { + if (check_feature_flags(ctx, d->decl_node) & OptInFeatureFlag_GlobalContext) { + c.scope->flags |= ScopeFlag_ContextDefined; + } else { + c.scope->flags &= ~ScopeFlag_ContextDefined; + } } global_group_check_edge(ctx, e); @@ -2242,6 +2291,7 @@ end:; // NOTE(bill): Add it to the list of checked entities if (is_lazy) { array_add(&ctx->info->entities, e); + lazy_mutex_depth -= 1; mutex_unlock(&ctx->info->lazy_mutex); } } diff --git a/src/check_expr.cpp b/src/check_expr.cpp index 2985c8c67..4f0feab8b 100644 --- a/src/check_expr.cpp +++ b/src/check_expr.cpp @@ -178,10 +178,10 @@ gb_internal void populate_check_did_you_mean_objc_entity(StringSet *set, Entity if (e->kind != Entity_TypeName) { return; } - if (e->TypeName.objc_metadata == nullptr) { + TypeNameObjCMetadata *objc_metadata = entity_objc_metadata(e); + if (objc_metadata == nullptr) { return; } - TypeNameObjCMetadata *objc_metadata = e->TypeName.objc_metadata; Type *t = base_type(e->type); GB_ASSERT(t->kind == Type_Struct); @@ -210,8 +210,8 @@ gb_internal void check_did_you_mean_objc_entity(String const &name, Entity *e, b ERROR_BLOCK(); GB_ASSERT(e->kind == Entity_TypeName); - GB_ASSERT(e->TypeName.objc_metadata != nullptr); - auto *objc_metadata = e->TypeName.objc_metadata; + auto *objc_metadata = entity_objc_metadata(e); + GB_ASSERT(objc_metadata != nullptr); MUTEX_GUARD(objc_metadata->mutex); StringSet set = {}; @@ -515,7 +515,8 @@ gb_internal bool find_or_generate_polymorphic_procedure(CheckerContext *old_c, E // NOTE(bill): This is slightly memory leaking if the type already exists // Maybe it's better to check with the previous types first? Type *final_proc_type = alloc_type_proc(scope, nullptr, 0, nullptr, 0, false, pt->calling_convention); - bool success = check_procedure_type(&nctx, final_proc_type, pt->node, &operands); + // NOTE: a clone, as other threads may be instantiating the same procedure, from the same AST + bool success = check_procedure_type(&nctx, final_proc_type, clone_ast(pt->node), &operands); if (!success) { return false; @@ -655,14 +656,7 @@ gb_internal bool find_or_generate_polymorphic_procedure(CheckerContext *old_c, E d->entity.store(entity); - AstFile *file = nullptr; - { - Scope *s = entity->scope; - while (s != nullptr && s->file == nullptr) { - file = s->file; - s = s->parent; - } - } + AstFile *file = base_entity->file; array_add(&gen_procs->procs, entity); rw_mutex_unlock(&gen_procs->mutex); // @local-mutex @@ -6116,7 +6110,7 @@ gb_internal Entity *check_selector(CheckerContext *c, Operand *operand, Ast *nod if (operand->type->kind == Type_Named && operand->type->Named.type_name && operand->type->Named.type_name->kind == Entity_TypeName && - operand->type->Named.type_name->TypeName.objc_metadata) { + entity_objc_metadata(operand->type->Named.type_name)) { check_did_you_mean_objc_entity(name, operand->type->Named.type_name, operand->mode == Addressing_Type); } else if (bt->kind == Type_Struct) { check_did_you_mean_type(name, bt->Struct.fields); @@ -8816,6 +8810,11 @@ gb_internal CallArgumentError check_polymorphic_record_type(CheckerContext *c, O CheckerContext ctx = *c; // NOTE(bill): We need to make sure the lookup scope for the record is the same as where it was created ctx.scope = polymorphic_record_parent_scope(original_type); + + if (original_type->Named.type_name && original_type->Named.type_name->file) { + ctx.file = original_type->Named.type_name->file; + ctx.pkg = ctx.file->pkg; + } GB_ASSERT(ctx.scope != nullptr); Type *bt = base_type(original_type); @@ -8830,7 +8829,9 @@ gb_internal CallArgumentError check_polymorphic_record_type(CheckerContext *c, O set_base_type(named_type, struct_type); check_open_scope(&ctx, node); + begin_filling_record(struct_type); check_struct_type(&ctx, struct_type, node, &ordered_operands, named_type, original_type, found_gen_types); + end_filling_record(struct_type); // check_struct_type released found_gen_types->mutex after publishing the instantiation. gen_types_locked = false; check_close_scope(&ctx); diff --git a/src/check_stmt.cpp b/src/check_stmt.cpp index ffc6f4cc0..dfb046da3 100644 --- a/src/check_stmt.cpp +++ b/src/check_stmt.cpp @@ -2355,23 +2355,7 @@ gb_internal void check_value_decl_stmt(CheckerContext *ctx, Ast *node, u32 mod_f } init_entity_foreign_library(ctx, e); - auto *fp = &ctx->checker->info.foreigns; - StringHashKey key = string_hash_string(name); - Entity **found = string_map_get(fp, key); - if (found) { - Entity *f = *found; - TokenPos pos = f->token.pos; - Type *this_type = base_type(e->type); - Type *other_type = base_type(f->type); - if (!signature_parameter_similar_enough(this_type, other_type)) { - error(e->token, - "Foreign entity '%.*s' previously declared elsewhere with a different type\n" - "\tat %s", - LIT(name), token_pos_to_string(pos)); - } - } else { - string_map_set(fp, key, e); - } + add_link_name_use(ctx->info, name, e, ctx->decl, LinkNameUse_Variable); } else if (e->flags & EntityFlag_Static) { if (vd->values.count > 0) { if (entity_count != vd->values.count) { diff --git a/src/check_type.cpp b/src/check_type.cpp index 61f5a7b3a..6c39472db 100644 --- a/src/check_type.cpp +++ b/src/check_type.cpp @@ -349,7 +349,9 @@ gb_internal void add_polymorphic_record_entity(CheckerContext *ctx, Ast *node, T e = alloc_entity_type_name(s, token, named_type); e->state = EntityState_Resolved; - e->file = ctx->file; + + // NOTE(bille): the generic's file as its token is not that of whichever instantiated it first + e->file = original_type->Named.type_name && original_type->Named.type_name->file ? original_type->Named.type_name->file : ctx->file; e->pkg = pkg; e->TypeName.original_type_for_parapoly = original_type; add_entity_use(ctx, node, e); @@ -770,40 +772,15 @@ gb_internal void check_struct_type(CheckerContext *ctx, Type *struct_type, Ast * } } - if (!struct_type->Struct.is_polymorphic) { - if (st->where_clauses.count > 0 && st->polymorphic_params == nullptr) { - error(st->where_clauses[0], "'where' clauses can only be used on structures with polymorphic parameters"); - } else { - bool where_clause_ok = evaluate_where_clauses(ctx, node, ctx->scope, &st->where_clauses, true); - gb_unused(where_clause_ok); - } - check_struct_fields(ctx, node, &struct_type->Struct.fields, &struct_type->Struct.tags, st->fields, min_field_count, struct_type, context); - - if (st->is_simple) { - bool success = true; - for (Entity *f : struct_type->Struct.fields) { - if (!is_type_nearly_simple_compare(f->type)) { - gbString s = type_to_string(f->type); - error(f->token, "'struct #simple' requires all fields to be at least 'nearly simple compare', got %s", s); - gb_string_free(s); - } - } - if (success) { - struct_type->Struct.is_simple = true; - } - } - - wait_signal_set(&struct_type->Struct.fields_wait_signal); - } #define ST_ALIGN(_name) if (st->_name != nullptr) { \ if (st->is_packed) { \ error(st->_name, "'#%s' cannot be applied with '#packed'", #_name); \ - return; \ - } \ - i64 align = 1; \ - if (check_custom_align(ctx, st->_name, &align, #_name)) { \ - struct_type->Struct.custom_##_name = align; \ + } else { \ + i64 align = 1; \ + if (check_custom_align(ctx, st->_name, &align, #_name)) { \ + struct_type->Struct.custom_##_name = align; \ + } \ } \ } @@ -836,6 +813,33 @@ gb_internal void check_struct_type(CheckerContext *ctx, Type *struct_type, Ast * } #undef ST_ALIGN + + if (!struct_type->Struct.is_polymorphic) { + if (st->where_clauses.count > 0 && st->polymorphic_params == nullptr) { + error(st->where_clauses[0], "'where' clauses can only be used on structures with polymorphic parameters"); + } else { + bool where_clause_ok = evaluate_where_clauses(ctx, node, ctx->scope, &st->where_clauses, true); + gb_unused(where_clause_ok); + } + check_struct_fields(ctx, node, &struct_type->Struct.fields, &struct_type->Struct.tags, st->fields, min_field_count, struct_type, context); + + if (st->is_simple) { + bool success = true; + for (Entity *f : struct_type->Struct.fields) { + if (!is_type_nearly_simple_compare(f->type)) { + gbString s = type_to_string(f->type); + error(f->token, "'struct #simple' requires all fields to be at least 'nearly simple compare', got %s", s); + gb_string_free(s); + } + } + if (success) { + struct_type->Struct.is_simple = true; + } + } + + wait_signal_set(&struct_type->Struct.fields_wait_signal); + } + } gb_internal void check_union_type(CheckerContext *ctx, Type *union_type, Ast *node, Array *poly_operands, Type *named_type, Type *original_type_for_poly, GenTypesData *poly_gen_types_to_unlock) { GB_ASSERT(is_type_union(union_type)); @@ -4622,6 +4626,9 @@ gb_internal Type *make_soa_struct_internal(CheckerContext *ctx, Ast *array_typ_e if (is_complete) { add_type_info_type(ctx, soa_struct); wait_signal_set(&soa_struct->Struct.fields_wait_signal); + } else if (global_group_soa_types != nullptr) { + // NOTE: no task waits on the element type, which could hold every thread of the pool + array_add(global_group_soa_types, soa_struct); } else { SoaTypeWorkerData *wd = permanent_alloc_item(); wd->ctx = *ctx; diff --git a/src/checker.cpp b/src/checker.cpp index 6bc02b9f6..6b7f10159 100644 --- a/src/checker.cpp +++ b/src/checker.cpp @@ -246,10 +246,10 @@ gb_internal Scope *create_scope(CheckerInfo *info, Scope *parent) { s->parent = parent; if (parent != nullptr && parent != builtin_pkg->scope) { - Scope *prev_head_child = parent->head_child.exchange(s, std::memory_order_acq_rel); - if (prev_head_child) { - s->next.store(prev_head_child, std::memory_order_release); - } + Scope *prev_head_child = parent->head_child.load(std::memory_order_acquire); + do { + s->next.store(prev_head_child, std::memory_order_relaxed); + } while (!parent->head_child.compare_exchange_weak(prev_head_child, s, std::memory_order_acq_rel, std::memory_order_acquire)); } if (parent != nullptr && parent->flags & ScopeFlag_ContextDefined) { @@ -272,6 +272,13 @@ gb_internal Scope *create_scope_from_file(CheckerInfo *info, AstFile *f) { s->file = f; f->scope = s; + bool global_context = f->feature_flags_set && (f->feature_flags & OptInFeatureFlag_GlobalContext) != 0; + if (global_context) { + s->flags |= ScopeFlag_ContextDefined; + } else { + s->flags &= ~ScopeFlag_ContextDefined; + } + return s; } @@ -425,10 +432,7 @@ gb_internal Entity *scope_lookup_current(Scope *s, InternedString name, u32 hash } -gb_global std::atomic in_single_threaded_checker_stage; - gb_internal void scope_lookup_parent(Scope *scope, InternedString name, Scope **scope_, Entity **entity_, u32 hash) { - bool is_single_threaded = in_single_threaded_checker_stage.load(std::memory_order_relaxed); if (scope != nullptr) { bool gone_thru_proc = false; bool gone_thru_package = false; @@ -437,9 +441,13 @@ gb_internal void scope_lookup_parent(Scope *scope, InternedString name, Scope ** } for (Scope *s = scope; s != nullptr; s = s->parent) { Entity *found = nullptr; - if (!is_single_threaded) rw_mutex_shared_lock(&s->mutex); - found = scope_map_get(&s->elements, name, hash); - if (!is_single_threaded) rw_mutex_shared_unlock(&s->mutex); + if (s->flags & ScopeFlag_ReadOnly) { + found = scope_map_get(&s->elements, name, hash); + } else { + rw_mutex_shared_lock(&s->mutex); + found = scope_map_get(&s->elements, name, hash); + rw_mutex_shared_unlock(&s->mutex); + } if (global_when_trial != nullptr) { found = global_when_trial_lookup(s, name, hash, found); } @@ -490,6 +498,7 @@ gb_internal Entity *scope_insert_with_name_no_mutex(Scope *s, InternedString nam if (name.value == 0) { return nullptr; } + GB_ASSERT_MSG((s->flags & ScopeFlag_ReadOnly) == 0, "%.*s", LIT(entity->token.string)); Entity *found = nullptr; Entity *result = nullptr; @@ -526,6 +535,7 @@ gb_internal Entity *scope_insert_with_name(Scope *s, InternedString name, u32 ha if (name.value == 0) { return nullptr; } + GB_ASSERT_MSG((s->flags & ScopeFlag_ReadOnly) == 0, "%.*s", LIT(entity->token.string)); Entity *found = nullptr; Entity *result = nullptr; @@ -568,11 +578,7 @@ gb_internal Entity *scope_insert(Scope *s, Entity *entity) { auto name = entity_interned_name(entity); u32 hash = entity->interned_name_hash.load(std::memory_order_relaxed); GB_ASSERT(hash != 0); - if (in_single_threaded_checker_stage.load(std::memory_order_relaxed)) { - return scope_insert_with_name_no_mutex(s, name, hash, entity); - } else { - return scope_insert_with_name(s, name, hash, entity); - } + return scope_insert_with_name(s, name, hash, entity); } gb_internal Entity *scope_insert_no_mutex(Scope *s, Entity *entity) { @@ -918,13 +924,9 @@ gb_internal void check_scope_usage(Checker *c, Scope *scope, u64 vet_flags) { gb_internal void add_dependency(CheckerInfo *info, DeclInfo *d, Entity *e) { - if (in_single_threaded_checker_stage.load(std::memory_order_relaxed)) { - ptr_set_add(&d->deps, e); - } else { - rw_mutex_lock(&d->deps_mutex); - ptr_set_add(&d->deps, e); - rw_mutex_unlock(&d->deps_mutex); - } + rw_mutex_lock(&d->deps_mutex); + ptr_set_add(&d->deps, e); + rw_mutex_unlock(&d->deps_mutex); } gb_internal void add_type_info_dependency(CheckerInfo *info, DeclInfo *d, Type *type) { if (d == nullptr || type == nullptr) { @@ -1649,7 +1651,7 @@ gb_internal void init_checker_info(CheckerInfo *i) { array_init(&i->definitions, a); array_init(&i->entities, a); map_init(&i->global_untyped); - string_map_init(&i->foreigns); + array_init(&i->link_names, heap_allocator()); type_set_init(&i->min_dep_type_info_set); map_init(&i->min_dep_type_info_index_map); @@ -1691,7 +1693,7 @@ gb_internal void destroy_checker_info(CheckerInfo *i) { array_free(&i->definitions); array_free(&i->entities); map_destroy(&i->global_untyped); - string_map_destroy(&i->foreigns); + array_free(&i->link_names); type_set_destroy(&i->min_dep_type_info_set); map_destroy(&i->min_dep_type_info_index_map); @@ -1917,13 +1919,12 @@ gb_internal ExprInfo *check_get_expr_info(CheckerContext *c, Ast *expr) { } return nullptr; } else { + // NOTE: read under the lock, as another thread's insert may move the entries rw_mutex_shared_lock(&c->info->global_untyped_mutex); ExprInfo **found = map_get(&c->info->global_untyped, expr); + ExprInfo *info = found ? *found : nullptr; rw_mutex_shared_unlock(&c->info->global_untyped_mutex); - if (found) { - return *found; - } - return nullptr; + return info; } } @@ -2023,8 +2024,6 @@ gb_internal void add_type_and_value(CheckerContext *ctx, Ast *expr, AddressingMo return; } - BlockingMutex *mutex = tav_mutex_for_node(expr); - /* Previous logic: BlockingMutex *mutex = &ctx->info->type_and_value_mutex; if (ctx->decl) { @@ -2034,10 +2033,11 @@ gb_internal void add_type_and_value(CheckerContext *ctx, Ast *expr, AddressingMo } */ - mutex_lock(mutex); Ast *prev_expr = nullptr; while (prev_expr != expr) { prev_expr = expr; + BlockingMutex *mutex = tav_mutex_for_node(expr); + mutex_lock(mutex); expr->tav.mode = mode; if (type != nullptr && expr->tav.type != nullptr && is_type_any(type) && is_type_untyped(expr->tav.type)) { @@ -2053,13 +2053,13 @@ gb_internal void add_type_and_value(CheckerContext *ctx, Ast *expr, AddressingMo } else if (mode == Addressing_Value && type != nullptr && is_type_proc(type)) { expr->tav.value = value; } + mutex_unlock(mutex); expr = unparen_expr(expr); if (expr == nullptr) { break; }; } - mutex_unlock(mutex); } gb_internal void add_entity_definition(CheckerInfo *i, Ast *identifier, Entity *entity) { @@ -2629,7 +2629,9 @@ gb_internal void check_procedure_later(Checker *c, ProcInfo *info) { if (global_procedure_body_in_worker_queue.load()) { thread_pool_add_task(check_proc_info_worker_proc, info); } else { + mutex_lock(&c->procs_to_check_mutex); array_add(&c->procs_to_check, info); + mutex_unlock(&c->procs_to_check_mutex); } if (DEBUG_CHECK_ALL_PROCEDURES) { @@ -3383,7 +3385,7 @@ gb_internal Array generate_entity_dependency_graph(CheckerInf } -gb_internal void check_single_global_entity(Checker *c, Entity *e, DeclInfo *d); +gb_internal void check_single_global_entity(Checker *c, Entity *e, DeclInfo *d, UntypedExprInfoMap *untyped=nullptr); gb_internal Entity *find_core_entity(Checker *c, String name) { @@ -5329,7 +5331,7 @@ gb_internal CheckerContext *create_checker_context(Checker *c) { return ctx; } -gb_internal void check_single_global_entity(Checker *c, Entity *e, DeclInfo *d) { +gb_internal void check_single_global_entity(Checker *c, Entity *e, DeclInfo *d, UntypedExprInfoMap *untyped) { GB_ASSERT(e != nullptr); if (e->state == EntityState_Resolved) { // NOTE: also an alias already overridden by what it aliases, which may have no `DeclInfo` @@ -5352,6 +5354,7 @@ gb_internal void check_single_global_entity(Checker *c, Entity *e, DeclInfo *d) GB_ASSERT(e->pkg != nullptr); ctx->decl = d; ctx->scope = d->scope; + ctx->untyped = untyped; if (pkg->kind == Package_Init) { if (e->kind != Entity_Procedure && e->token.string == "main") { @@ -7164,6 +7167,22 @@ gb_internal void handle_raddbg_type_view(Checker *c, RaddbgTypeView const &type_ array_add(&c->info.raddbg_type_views, RaddbgTypeView{type, view}); } +gb_internal GB_COMPARE_PROC(raddbg_type_view_cmp) { + RaddbgTypeView const *x = cast(RaddbgTypeView const *)a; + RaddbgTypeView const *y = cast(RaddbgTypeView const *)b; + Entity *xe = (x->type && x->type->kind == Type_Named) ? x->type->Named.type_name : nullptr; + Entity *ye = (y->type && y->type->kind == Type_Named) ? y->type->Named.type_name : nullptr; + if (xe != nullptr && ye != nullptr && xe != ye) { + return entity_source_order_cmp(xe, ye); + } + u64 xh = type_hash_canonical_type(x->type); + u64 yh = type_hash_canonical_type(y->type); + if (xh != yh) { + return xh < yh ? -1 : +1; + } + return string_compare(x->view, y->view); +} + gb_internal void check_objc_context_provider_procedures(Checker *c) { for (Entity *e = nullptr; mpsc_dequeue(&c->procs_with_objc_context_provider_to_check, &e); /**/) { GB_ASSERT(e->kind == Entity_TypeName); @@ -7343,18 +7362,18 @@ gb_internal void check_update_dependency_tree_for_procedures(Checker *c) { #else gb_internal void check_walk_all_dependencies(DeclInfo *decl); -gb_internal WORKER_TASK_PROC(check_walk_all_dependencies_worker_proc) { - if (data == nullptr) { - return 0; - } - DeclInfo *decl = cast(DeclInfo *)data; - +// NOTE: post-order, so a declaration has the dependencies of all those nested in it before they are added to its parent +gb_internal void check_walk_all_dependencies_post_order(DeclInfo *decl) { for (DeclInfo *child = decl->next_child; child != nullptr; child = child->next_sibling) { - thread_pool_add_task(check_walk_all_dependencies_worker_proc, child); - check_walk_all_dependencies(child); + check_walk_all_dependencies_post_order(child); } - add_deps_from_child_to_parent(decl); +} + +gb_internal WORKER_TASK_PROC(check_walk_all_dependencies_worker_proc) { + if (data != nullptr) { + check_walk_all_dependencies_post_order(cast(DeclInfo *)data); + } return 0; } @@ -7488,6 +7507,17 @@ gb_internal void check_parsed_files(Checker *c) { TIME_SECTION("export entities - post"); check_export_entities(c); + // NOTE: no global name is declared from here on, so their scopes are read without locking + for (AstPackage *pkg : c->parser->packages) { + pkg->scope->flags |= ScopeFlag_ReadOnly; + for (AstFile *f : pkg->files) { + f->scope->flags |= ScopeFlag_ReadOnly; + } + } + builtin_pkg->scope->flags |= ScopeFlag_ReadOnly; + intrinsics_pkg->scope->flags |= ScopeFlag_ReadOnly; + config_pkg->scope->flags |= ScopeFlag_ReadOnly; + TIME_SECTION("add entities from packages"); check_merge_queues_into_arrays(c); @@ -7622,6 +7652,9 @@ gb_internal void check_parsed_files(Checker *c) { debugf("Total Procedure Bodies Checked: %td\n", total_bodies_checked.load(std::memory_order_relaxed)); + TIME_SECTION("check unique link names"); + check_link_name_uses(c); + TIME_SECTION("check unique package names"); bool package_names_are_unique = check_unique_package_names(c); @@ -7720,8 +7753,16 @@ gb_internal void check_parsed_files(Checker *c) { } TIME_SECTION("collate type info stuff"); - for (RaddbgTypeView type_view; mpsc_dequeue(&c->info.raddbg_type_views_queue, &type_view); /**/) { - handle_raddbg_type_view(c, type_view); + { + auto views = array_make(heap_allocator()); + defer (array_free(&views)); + for (RaddbgTypeView type_view; mpsc_dequeue(&c->info.raddbg_type_views_queue, &type_view); /**/) { + array_add(&views, type_view); + } + array_sort(views, raddbg_type_view_cmp); + for (RaddbgTypeView const &type_view : views) { + handle_raddbg_type_view(c, type_view); + } } diff --git a/src/checker.hpp b/src/checker.hpp index 633790405..78f579c2d 100644 --- a/src/checker.hpp +++ b/src/checker.hpp @@ -269,6 +269,20 @@ struct ProcInfo { }; +enum LinkNameUseKind : u8 { + LinkNameUse_ForeignProcedure, + LinkNameUse_Procedure, // exported or with a link name + LinkNameUse_Variable, // foreign or exported +}; + +struct LinkNameUse { + String name; + Entity * entity; + DeclInfo * decl; + LinkNameUseKind kind; +}; + + enum { DEFAULT_SCOPE_CAPACITY = 32 }; @@ -461,6 +475,19 @@ gb_internal Entity *scope_map_get(ScopeMap *m, InternedString key, u32 hash) { } } +// NOTE: the key must be present; never grows, so lookups that do not lock see either value +gb_internal void scope_map_replace(ScopeMap *m, InternedString key, u32 hash, Entity *value) { + u32 mask = m->cap-1; + for (u32 pos = hash & mask;; pos = (pos + 1) & mask) { + ScopeMapSlot *s = &m->slots[pos]; + GB_ASSERT(s->hash != 0); + if (s->hash == hash && m->keys[pos] == key) { + s->value = value; + return; + } + } +} + gb_internal void scope_map_clear(ScopeMap *m) { gb_memset(m->slots, 0, gb_size_of(*m->slots) * m->cap); m->count = 0; @@ -548,6 +575,7 @@ enum ScopeFlag : i32 { ScopeFlag_Type = 1<<7, ScopeFlag_HasBeenImported = 1<<10, // This is only applicable to file scopes + ScopeFlag_ReadOnly = 1<<11, // file, package and universe scopes once every global is declared: read without locking ScopeFlag_ContextDefined = 1<<16, }; @@ -761,7 +789,9 @@ struct CheckerInfo { // TypeSet type_info_set; BlockingMutex foreign_mutex; // NOT recursive - StringMap foreigns; + Array link_names; // checked for clashes once everything is checked, see `check_link_name_uses` + + BlockingMutex entry_point_mutex; MPSCQueue definition_queue; MPSCQueue entity_queue; @@ -868,6 +898,7 @@ struct Checker { MPSCQueue procs_with_deferred_to_check; MPSCQueue procs_with_objc_context_provider_to_check; + BlockingMutex procs_to_check_mutex; Array procs_to_check; BlockingMutex nested_proc_lits_mutex; @@ -921,6 +952,9 @@ gb_internal void check_add_foreign_import_decl(CheckerContext *c, Ast *decl); gb_internal void check_entity_decl(CheckerContext *c, Entity *e, DeclInfo *d, Type *named_type); gb_internal void global_group_check_edge(CheckerContext *ctx, Entity *e); +// While a group of global entities is checked: its incomplete '#soa' types, completed by the same thread +gb_thread_local Array *global_group_soa_types; + struct GlobalWhenTrialEntityScope { struct GlobalWhenTrial *trial; i32 mute_depth; diff --git a/src/checker_global.cpp b/src/checker_global.cpp index ccc212505..fd8b51130 100644 --- a/src/checker_global.cpp +++ b/src/checker_global.cpp @@ -39,6 +39,7 @@ gb_internal void global_import_stage_end(GlobalImportStagePart part, u64 start) } } +gb_global std::atomic in_global_entity_stage; // to tell the checks of the global stage from those during 'when' resolution gb_global BlockingMutex global_entity_time_mutex; gb_global PtrMap global_entity_times; gb_thread_local u64 global_entity_child_ticks; @@ -68,7 +69,7 @@ gb_internal void global_entity_timing_end(GlobalEntityTimingFrame const &f, Enti u64 self = total - gb_min(total, global_entity_child_ticks); global_entity_child_ticks = f.saved_child_ticks + total; - bool in_global_loop = in_single_threaded_checker_stage.load(std::memory_order_relaxed); + bool in_global_loop = in_global_entity_stage.load(std::memory_order_relaxed); MUTEX_GUARD(&global_entity_time_mutex); GlobalEntityTime *found = map_get(&global_entity_times, e); @@ -1321,10 +1322,15 @@ gb_internal void global_group_check_edge(CheckerContext *ctx, Entity *e) { // NOTE: members in a fixed order, as which member of a cycle is entered first can decide whether it checks, // e.g. an enum whose values are `union_variant_index`es of a union with pointers back to it +// NOTE: a group's untyped expressions and '#soa' types are its own, so it touches no shared queue meanwhile gb_internal void check_global_group(Checker *c, GlobalGroupGraph *g, i32 gi) { GlobalGroup *group = &g->groups[gi]; i32 *members = g->members.data + group->start; + UntypedExprInfoMap untyped = {}; + auto soa_types = array_make(heap_allocator()); + global_group_soa_types = &soa_types; + g->current_group = gi; for (i32 k = 0; k < group->count; k++) { Entity *e = g->nodes[members[k]]; @@ -1334,17 +1340,27 @@ gb_internal void check_global_group(Checker *c, GlobalGroupGraph *g, i32 gi) { } g->current_entity = e; GlobalEntityTimingFrame timing_frame = global_entity_timing_begin(e); - check_single_global_entity(c, e, e->decl_info); + check_single_global_entity(c, e, e->decl_info, &untyped); if (e->type != nullptr && is_type_typed(e->type)) { - for (Type *t = nullptr; mpsc_dequeue(&c->soa_types_to_complete, &t); /**/) { + for (Type *t : soa_types) { complete_soa_type(c, t, false); } + array_clear(&soa_types); (void)type_size_of(e->type); (void)type_align_of(e->type); } global_entity_timing_end(timing_frame, e); } + for (Type *t : soa_types) { + complete_soa_type(c, t, false); + } + global_group_soa_types = nullptr; + array_free(&soa_types); + + add_untyped_expressions(&c->info, &untyped); + map_destroy(&untyped); + group->done = true; g->current_group = -1; g->current_entity = nullptr; @@ -1432,7 +1448,7 @@ gb_internal void destroy_global_groups(GlobalGroupGraph *g) { } gb_internal void check_all_global_entities(Checker *c) { - in_single_threaded_checker_stage.store(true, std::memory_order_relaxed); + in_global_entity_stage.store(true, std::memory_order_relaxed); // NOTE(bill): the runtime types the checker looks up by name rather than through a declaration init_preload(c); @@ -1444,6 +1460,10 @@ gb_internal void check_all_global_entities(Checker *c) { } } + for (Type *t = nullptr; mpsc_dequeue(&c->soa_types_to_complete, &t); /**/) { + complete_soa_type(c, t, false); + } + TIME_SECTION("check all global entities - build groups"); GlobalGroupGraph *g = &global_groups; build_global_groups(c, g); @@ -1459,7 +1479,7 @@ gb_internal void check_all_global_entities(Checker *c) { gb_exit(1); } - in_single_threaded_checker_stage.store(false, std::memory_order_relaxed); + in_global_entity_stage.store(false, std::memory_order_relaxed); } diff --git a/src/entity.cpp b/src/entity.cpp index b5c1c336b..845e2b910 100644 --- a/src/entity.cpp +++ b/src/entity.cpp @@ -142,6 +142,10 @@ enum ProcedureOptimizationMode : u8 { BlockingMutex global_type_name_objc_metadata_mutex; +struct TypeNameObjCMetadata; + +gb_internal TypeNameObjCMetadata *entity_objc_metadata(struct Entity *e); + struct TypeNameObjCMetadataEntry { InternedString interned; Entity *entity; diff --git a/src/llvm_backend.cpp b/src/llvm_backend.cpp index a0d87630c..ce6a8820a 100644 --- a/src/llvm_backend.cpp +++ b/src/llvm_backend.cpp @@ -1576,7 +1576,13 @@ gb_internal void lb_finalize_objc_names(lbGenerator *gen, lbProcedure *p) { auto class_impls = array_make(temporary_allocator(), 0, 16); // Register all class implementations unconditionally, even if not statically referenced + auto implementations = array_make(temporary_allocator(), 0, 16); for (Entity *e = {}; mpsc_dequeue(&gen->info->objc_class_implementations, &e); /**/) { + array_add(&implementations, e); + } + array_sort(implementations, init_procedures_cmp); + + for (Entity *e : implementations) { GB_ASSERT(e->kind == Entity_TypeName && e->TypeName.objc_is_implementation); lb_handle_objc_find_or_register_class(p, e->TypeName.objc_class_name, e->type); diff --git a/src/types.cpp b/src/types.cpp index 6408669b0..c65a7c430 100644 --- a/src/types.cpp +++ b/src/types.cpp @@ -338,7 +338,6 @@ gb_global String const type_strings[] = { enum TypeFlag : u32 { TypeFlag_Polymorphic = 1<<1, TypeFlag_PolySpecialized = 1<<2, - TypeFlag_InProcessOfCheckingPolymorphic = 1<<3, }; struct Type { @@ -2494,13 +2493,24 @@ gb_internal TypeTuple *get_record_polymorphic_params(Type *t) { } +gb_internal TypeNameObjCMetadata *entity_objc_metadata(Entity *e) { + GB_ASSERT(e->kind == Entity_TypeName); + mutex_lock(&global_type_name_objc_metadata_mutex); + TypeNameObjCMetadata *md = e->TypeName.objc_metadata; + Type *original = e->TypeName.original_type_for_parapoly; + if (md == nullptr && original != nullptr && original->kind == Type_Named && original->Named.type_name != nullptr) { + md = original->Named.type_name->TypeName.objc_metadata; + } + mutex_unlock(&global_type_name_objc_metadata_mutex); + return md; +} + +gb_internal gb_thread_local Array is_type_polymorphic_named_stack; + gb_internal bool is_type_polymorphic(Type *t, bool or_specialized=false) { if (t == nullptr) { return false; } - if (t->flags & TypeFlag_InProcessOfCheckingPolymorphic) { - return false; - } switch (t->kind) { case Type_Generic: @@ -2508,10 +2518,18 @@ gb_internal bool is_type_polymorphic(Type *t, bool or_specialized=false) { case Type_Named: { - u32 flags = t->flags; - t->flags |= TypeFlag_InProcessOfCheckingPolymorphic; + Array &stack = is_type_polymorphic_named_stack; + for (Type *named : stack) { + if (named == t) { + return false; + } + } + if (stack.allocator.proc == nullptr) { + array_init(&stack, heap_allocator()); + } + array_add(&stack, t); bool ok = is_type_polymorphic(t->Named.base, or_specialized); - t->flags = flags; + array_pop(&stack); return ok; } @@ -3815,8 +3833,7 @@ gb_internal Selection lookup_field_with_selection(Type *type_, InternedString fi if (has_type_got_objc_class_attribute(original_type) && original_type->kind == Type_Named) { Entity *e = original_type->Named.type_name; GB_ASSERT(e->kind == Entity_TypeName); - if (e->TypeName.objc_metadata) { - auto *md = e->TypeName.objc_metadata; + if (auto *md = entity_objc_metadata(e)) { mutex_lock(md->mutex); defer (mutex_unlock(md->mutex)); for (TypeNameObjCMetadataEntry const &entry : md->type_entries) { @@ -3907,8 +3924,7 @@ gb_internal Selection lookup_field_with_selection(Type *type_, InternedString fi if (has_type_got_objc_class_attribute(original_type) && original_type->kind == Type_Named) { Entity *e = original_type->Named.type_name; GB_ASSERT(e->kind == Entity_TypeName); - if (e->TypeName.objc_metadata) { - auto *md = e->TypeName.objc_metadata; + if (auto *md = entity_objc_metadata(e)) { mutex_lock(md->mutex); defer (mutex_unlock(md->mutex)); for (TypeNameObjCMetadataEntry const &entry : md->value_entries) { @@ -4405,6 +4421,35 @@ gb_internal i64 type_target_max_align(void) { return max_align; } +// Polymorphic record instances being filled by this thread, which another may already have found in the cache +gb_internal gb_thread_local Array records_being_filled; + +gb_internal void begin_filling_record(Type *t) { + if (records_being_filled.allocator.proc == nullptr) { + array_init(&records_being_filled, heap_allocator()); + } + array_add(&records_being_filled, t); +} + +gb_internal void end_filling_record(Type *t) { + GB_ASSERT(records_being_filled.count > 0 && records_being_filled[records_being_filled.count-1] == t); + array_pop(&records_being_filled); +} + +gb_internal gb_thread_local i32 lazy_mutex_depth; + +gb_internal void wait_for_struct_fields(Type *t) { + if (t->Struct.polymorphic_parent == nullptr || t->Struct.fields_wait_signal.futex.load() != 0 || lazy_mutex_depth > 0) { + return; + } + for (Type *r : records_being_filled) { + if (r == t) { + return; + } + } + wait_signal_until_available(&t->Struct.fields_wait_signal); +} + gb_internal i64 type_align_of_internal(Type *t, TypePath *path) { GB_ASSERT(path != nullptr); if (t->failure) { @@ -4529,6 +4574,7 @@ gb_internal i64 type_align_of_internal(Type *t, TypePath *path) { } break; case Type_Struct: { + wait_for_struct_fields(t); if (t->Struct.custom_align > 0) { return gb_max(t->Struct.custom_align, 1); } @@ -4643,6 +4689,7 @@ gb_internal i64 *type_set_offsets_of(Slice const &fields, bool is_pack gb_internal bool type_set_offsets(Type *t) { t = base_type(t); if (t->kind == Type_Struct) { + wait_for_struct_fields(t); // if (t->Struct.are_offsets_being_processed.load()) { // return true; // } @@ -4847,6 +4894,7 @@ gb_internal i64 type_size_of_internal(Type *t, TypePath *path) { case Type_Struct: { + wait_for_struct_fields(t); if (t->Struct.is_raw_union) { i64 count = t->Struct.fields.count; i64 align = type_align_of_internal(t, path); From 5f579a1a10eeefd6c282f8f1f8af9c20eca7ca8a Mon Sep 17 00:00:00 2001 From: gingerBill Date: Thu, 1 Oct 2026 21:55:25 +0100 Subject: [PATCH 10/17] Fix numerous race conditions: clone polymorphic defaults per call, evaluate foreign import attributes after collection, own dependencies for record instances, sorted Objective-C emission, no debug line for anonymous records --- src/check_expr.cpp | 8 ++++++- src/check_type.cpp | 1 + src/checker.cpp | 49 +++++++++++++++++++++++--------------- src/llvm_backend.cpp | 49 +++++++++++++++++++++++++++++++++++--- src/llvm_backend_debug.cpp | 16 +++++++------ src/types.cpp | 2 +- 6 files changed, 94 insertions(+), 31 deletions(-) diff --git a/src/check_expr.cpp b/src/check_expr.cpp index 4f0feab8b..f655b206e 100644 --- a/src/check_expr.cpp +++ b/src/check_expr.cpp @@ -6867,7 +6867,8 @@ gb_internal CallArgumentError check_call_arguments_internal(CheckerContext *c, A // then the default is materialized against the concrete type (reporting a clear error if it does not fit). ordered_operands[i].mode = Addressing_Invalid; ordered_operands[i].type = t_invalid; - ordered_operands[i].expr = e->Variable.param_value.original_ast_expr; + // NOTE(bill): Check a clone, as the callee's default expression is shared by every call site. + ordered_operands[i].expr = clone_ast(e->Variable.param_value.original_ast_expr); ordered_operands[i].deferred_untyped_arg = true; } else { ordered_operands[i].mode = Addressing_Value; @@ -8802,6 +8803,7 @@ gb_internal CallArgumentError check_polymorphic_record_type(CheckerContext *c, O Entity *found_entity = find_polymorphic_record_entity(found_gen_types, param_count, ordered_operands); if (found_entity) { + add_declaration_dependency(c, found_entity); operand->mode = Addressing_Type; operand->type = found_entity->type; return err; @@ -8810,6 +8812,9 @@ gb_internal CallArgumentError check_polymorphic_record_type(CheckerContext *c, O CheckerContext ctx = *c; // NOTE(bill): We need to make sure the lookup scope for the record is the same as where it was created ctx.scope = polymorphic_record_parent_scope(original_type); + // NOTE(bill): the instance's members are only checked by its first use, so their dependencies are + // the instance's own, and each use depends on the instance instead + ctx.decl = make_decl_info(ctx.scope, nullptr); if (original_type->Named.type_name && original_type->Named.type_name->file) { ctx.file = original_type->Named.type_name->file; @@ -8851,6 +8856,7 @@ gb_internal CallArgumentError check_polymorphic_record_type(CheckerContext *c, O GB_PANIC("Unsupported parametric polymorphic record type"); } + add_declaration_dependency(c, named_type->Named.type_name); operand->mode = Addressing_Type; operand->type = named_type; } diff --git a/src/check_type.cpp b/src/check_type.cpp index 6c39472db..247f52ea3 100644 --- a/src/check_type.cpp +++ b/src/check_type.cpp @@ -354,6 +354,7 @@ gb_internal void add_polymorphic_record_entity(CheckerContext *ctx, Ast *node, T e->file = original_type->Named.type_name && original_type->Named.type_name->file ? original_type->Named.type_name->file : ctx->file; e->pkg = pkg; e->TypeName.original_type_for_parapoly = original_type; + e->decl_info = ctx->decl; add_entity_use(ctx, node, e); } diff --git a/src/checker.cpp b/src/checker.cpp index 6b7f10159..95961c8a1 100644 --- a/src/checker.cpp +++ b/src/checker.cpp @@ -5766,6 +5766,23 @@ gb_internal void check_foreign_import_fullpaths(Checker *c) { GB_ASSERT(ctx.scope == e->scope); + AttributeContext ac = {}; + check_decl_attributes(&ctx, fl->attributes, foreign_import_decl_attribute, &ac); + if (ac.require_declaration) { + mpsc_enqueue(&ctx.info->required_foreign_imports_through_force_queue, e); + add_entity_use(&ctx, nullptr, e); + } + if (ac.foreign_import_priority_index != 0) { + e->LibraryName.priority_index = ac.foreign_import_priority_index; + } + if (ac.ignore_duplicates) { + e->LibraryName.ignore_duplicates = true; + } + String extra_linker_flags = string_trim_whitespace(ac.extra_linker_flags); + if (extra_linker_flags.len != 0) { + e->LibraryName.extra_linker_flags = extra_linker_flags; + } + if (fl->fullpaths.count == 0) { String base_dir = dir_from_path(decl->file()->fullpath); @@ -5878,11 +5895,21 @@ gb_internal void check_add_foreign_import_decl(CheckerContext *ctx, Ast *decl) { GB_ASSERT(fl->library_name.pos.line != 0); fl->library_name.string = library_name; - AttributeContext ac = {}; - check_decl_attributes(ctx, fl->attributes, foreign_import_decl_attribute, &ac); + // NOTE(bill): Only 'export' is needed to declare the entity; the attribute values are evaluated in + // `check_foreign_import_fullpaths` as the globals they may name are not all collected yet + bool is_export = false; + for (Ast *attr : fl->attributes) { + if (attr->kind != Ast_Attribute) continue; + for (Ast *elem : attr->Attribute.elems) { + Ast *name = elem->kind == Ast_FieldValue ? elem->FieldValue.field : elem; + if (name->kind == Ast_Ident && name->Ident.token.string == "export") { + is_export = true; + } + } + } Scope *scope = parent_scope; - if (ac.is_export) { + if (is_export) { scope = parent_scope->parent; } @@ -5892,22 +5919,6 @@ gb_internal void check_add_foreign_import_decl(CheckerContext *ctx, Ast *decl) { add_entity_flags_from_file(ctx, e, parent_scope); add_entity(ctx, scope, nullptr, e); - - if (ac.require_declaration) { - mpsc_enqueue(&ctx->info->required_foreign_imports_through_force_queue, e); - add_entity_use(ctx, nullptr, e); - } - if (ac.foreign_import_priority_index != 0) { - e->LibraryName.priority_index = ac.foreign_import_priority_index; - } - if (ac.ignore_duplicates) { - e->LibraryName.ignore_duplicates = true; - } - String extra_linker_flags = string_trim_whitespace(ac.extra_linker_flags); - if (extra_linker_flags.len != 0) { - e->LibraryName.extra_linker_flags = extra_linker_flags; - } - mpsc_enqueue(&ctx->info->foreign_imports_to_check_fullpaths, e); } diff --git a/src/llvm_backend.cpp b/src/llvm_backend.cpp index ce6a8820a..05ce7ae17 100644 --- a/src/llvm_backend.cpp +++ b/src/llvm_backend.cpp @@ -1559,6 +1559,28 @@ gb_internal void lb_register_objc_thing( } } +gb_internal GB_COMPARE_PROC(objc_global_cmp) { + lbObjCGlobal const *x = cast(lbObjCGlobal const *)a; + lbObjCGlobal const *y = cast(lbObjCGlobal const *)b; + int cmp = string_compare(x->name, y->name); + if (cmp == 0) { + cmp = (x->class_impl_type != nullptr) - (y->class_impl_type != nullptr); + } + return cmp; +} + +gb_internal GB_COMPARE_PROC(objc_class_type_cmp) { + Type *x = *cast(Type **)a; + Type *y = *cast(Type **)b; + return entity_source_order_cmp(x->Named.type_name, y->Named.type_name); +} + +gb_internal GB_COMPARE_PROC(objc_method_data_cmp) { + ObjcMethodData const *x = cast(ObjcMethodData const *)a; + ObjcMethodData const *y = cast(ObjcMethodData const *)b; + return entity_source_order_cmp(x->proc_entity, y->proc_entity); +} + gb_internal void lb_finalize_objc_names(lbGenerator *gen, lbProcedure *p) { if (p == nullptr) { return; @@ -1612,11 +1634,18 @@ gb_internal void lb_finalize_objc_names(lbGenerator *gen, lbProcedure *p) { } } + // NOTE(bill): the queues are filled by the codegen threads and the set is in address order, so all are sorted + auto class_types = array_make(temporary_allocator(), 0, class_set.count); for (auto pair : class_set) { - Entity *e = pair.type->Named.type_name; + array_add(&class_types, pair.type); + } + array_sort(class_types, objc_class_type_cmp); + + for (Type *class_type : class_types) { + Entity *e = class_type->Named.type_name; GB_ASSERT(e->kind == Entity_TypeName); auto &tn = e->TypeName; - Type *class_impl = !tn.objc_is_implementation ? nullptr : pair.type; + Type *class_impl = !tn.objc_is_implementation ? nullptr : class_type; lb_handle_objc_find_or_register_class(p, tn.objc_class_name, class_impl); if (build_context.bedrock) { @@ -1626,6 +1655,11 @@ gb_internal void lb_finalize_objc_names(lbGenerator *gen, lbProcedure *p) { for (lbObjCGlobal g = {}; mpsc_dequeue(&gen->objc_classes, &g); /**/) { array_add(&referenced_classes, g); } + array_sort(referenced_classes, objc_global_cmp); + + for (auto &kv : m->info->objc_method_implementations) { + array_sort(kv.value, objc_method_data_cmp); + } // Add all class globals to a map so that we can look them up dynamically // in order to resolve out-of-order because classes that are being implemented @@ -1662,7 +1696,12 @@ gb_internal void lb_finalize_objc_names(lbGenerator *gen, lbProcedure *p) { } // Now we can register all referenced selectors + auto selectors = array_make(temporary_allocator()); for (lbObjCGlobal g = {}; mpsc_dequeue(&gen->objc_selectors, &g); /**/) { + array_add(&selectors, g); + } + array_sort(selectors, objc_global_cmp); + for (lbObjCGlobal const &g : selectors) { lb_register_objc_thing(handled, m, args, class_impls, global_class_map, p, g, "sel_registerName"); } @@ -1943,8 +1982,12 @@ gb_internal void lb_finalize_objc_names(lbGenerator *gen, lbProcedure *p) { } // Register ivar offsets for any `objc_ivar_get` expressions emitted. + auto ivars = array_make(temporary_allocator(), 0, ivar_map.count); for (auto const& kv : ivar_map) { - lbObjCGlobal const& g = kv.value; + array_add(&ivars, kv.value); + } + array_sort(ivars, objc_global_cmp); + for (lbObjCGlobal const& g : ivars) { lbAddr ivar_addr = {}; lbValue *found = string_map_get(&m->members, g.global_name); diff --git a/src/llvm_backend_debug.cpp b/src/llvm_backend_debug.cpp index d3e63cbf7..b70071e5b 100644 --- a/src/llvm_backend_debug.cpp +++ b/src/llvm_backend_debug.cpp @@ -65,8 +65,10 @@ gb_internal LLVMMetadataRef lb_debug_end_location_from_ast(lbProcedure *p, Ast * return lb_debug_location_from_token_pos(p, ast_end_token(node).pos); } -gb_internal void lb_debug_file_line(lbModule *m, Ast *node, LLVMMetadataRef *file, unsigned *line) { - if (*file == nullptr) { +// NOTE(bill): not for an anonymous type, as identical ones are interchangeable, and which one is used +// (e.g. by a polymorphic instance) depends on the checking order +gb_internal void lb_debug_file_line(lbModule *m, Type *type, Ast *node, LLVMMetadataRef *file, unsigned *line) { + if (*file == nullptr && type->kind == Type_Named) { if (node) { *file = lb_get_llvm_metadata(m, node->file()); *line = cast(unsigned)ast_token(node).pos.line; @@ -192,7 +194,7 @@ gb_internal LLVMMetadataRef lb_debug_basic_struct(lbModule *m, String const &nam gb_internal LLVMMetadataRef lb_debug_struct(lbModule *m, Type *type, Type *bt, String name, LLVMMetadataRef scope, LLVMMetadataRef file, unsigned line) { GB_ASSERT(bt->kind == Type_Struct); - lb_debug_file_line(m, bt->Struct.node, &file, &line); + lb_debug_file_line(m, type, bt->Struct.node, &file, &line); unsigned tag = DW_TAG_structure_type; if (is_type_raw_union(bt)) { @@ -476,7 +478,7 @@ gb_internal LLVMMetadataRef lb_debug_union(lbModule *m, Type *type, String name, Type *bt = base_type(type); GB_ASSERT(bt->kind == Type_Union); - lb_debug_file_line(m, bt->Union.node, &file, &line); + lb_debug_file_line(m, type, bt->Union.node, &file, &line); u64 size_in_bits = 8*type_size_of(bt); u32 align_in_bits = 8*cast(u32)type_align_of(bt); @@ -560,7 +562,7 @@ gb_internal LLVMMetadataRef lb_debug_bitset(lbModule *m, Type *type, String name Type *bt = base_type(type); GB_ASSERT(bt->kind == Type_BitSet); - lb_debug_file_line(m, bt->BitSet.node, &file, &line); + lb_debug_file_line(m, type, bt->BitSet.node, &file, &line); u64 size_in_bits = 8*type_size_of(bt); u32 align_in_bits = 8*cast(u32)type_align_of(bt); @@ -641,7 +643,7 @@ gb_internal LLVMMetadataRef lb_debug_bitfield(lbModule *m, Type *type, String na Type *bt = base_type(type); GB_ASSERT(bt->kind == Type_BitField); - lb_debug_file_line(m, bt->BitField.node, &file, &line); + lb_debug_file_line(m, type, bt->BitField.node, &file, &line); u64 size_in_bits = 8*type_size_of(bt); u32 align_in_bits = 8*cast(u32)type_align_of(bt); @@ -682,7 +684,7 @@ gb_internal LLVMMetadataRef lb_debug_enum(lbModule *m, Type *type, String name, Type *bt = base_type(type); GB_ASSERT(bt->kind == Type_Enum); - lb_debug_file_line(m, bt->Enum.node, &file, &line); + lb_debug_file_line(m, type, bt->Enum.node, &file, &line); u64 size_in_bits = 8*type_size_of(bt); u32 align_in_bits = 8*cast(u32)type_align_of(bt); diff --git a/src/types.cpp b/src/types.cpp index c65a7c430..aad687ac7 100644 --- a/src/types.cpp +++ b/src/types.cpp @@ -353,7 +353,7 @@ struct Type { std::atomic cached_align; std::atomic canonical_hash; std::atomic flags; // TypeFlag - bool failure; + std::atomic failure; }; // IMPORTANT NOTE(bill): This must match the same as the in core.odin From c494c7a503712c6c247ccd87b1dc0b08374fd0ba Mon Sep 17 00:00:00 2001 From: gingerBill Date: Thu, 1 Oct 2026 23:17:34 +0100 Subject: [PATCH 11/17] Check global entity groups in parallel * CAS-claimed entities, cycle-safe waits for entities and records, lazy entities without lazy_mutex * Build union type names only on error, take the error block only to report proc group overload errors --- src/check_decl.cpp | 55 +++--- src/check_expr.cpp | 25 ++- src/check_stmt.cpp | 4 +- src/check_type.cpp | 12 +- src/checker.cpp | 4 + src/checker.hpp | 4 +- src/checker_global.cpp | 176 +++++++++++------- src/entity.cpp | 1 + src/thread_pool.cpp | 41 ++++ src/threading.cpp | 4 + src/types.cpp | 44 ++--- tests/issues/run.bat | 2 + tests/issues/run.sh | 2 + tests/issues/test_issue_7566.odin | 36 ++++ .../test_issue_foreign_import_attributes.odin | 13 ++ ...test_issue_global_when_cycle_accepted.odin | 28 +++ ...est_issue_global_when_cycle_ambiguous.odin | 5 + 17 files changed, 316 insertions(+), 140 deletions(-) create mode 100644 tests/issues/test_issue_7566.odin create mode 100644 tests/issues/test_issue_foreign_import_attributes.odin create mode 100644 tests/issues/test_issue_global_when_cycle_accepted.odin create mode 100644 tests/issues/test_issue_global_when_cycle_ambiguous.odin diff --git a/src/check_decl.cpp b/src/check_decl.cpp index 4a40ba7a4..008959936 100644 --- a/src/check_decl.cpp +++ b/src/check_decl.cpp @@ -1953,6 +1953,7 @@ gb_internal void check_proc_group_decl(CheckerContext *ctx, Entity *pg_entity, D GB_ASSERT(p != q); bool is_invalid = false; + bool different_results = false; TokenPos pos = q->token.pos; @@ -1960,9 +1961,6 @@ gb_internal void check_proc_group_decl(CheckerContext *ctx, Entity *pg_entity, D continue; } - - ERROR_BLOCK(); - if (q->flags & EntityFlag_Disabled) { continue; } @@ -1984,21 +1982,18 @@ gb_internal void check_proc_group_decl(CheckerContext *ctx, Entity *pg_entity, D if (!both_have_where_clauses) switch (kind) { case ProcOverload_Identical: - error(p->token, "Overloaded procedure '%.*s' has the same type as another procedure in the procedure group '%.*s'", LIT(name), LIT(proc_group_name)); is_invalid = true; break; // case ProcOverload_CallingConvention: - // error(p->token, "Overloaded procedure '%.*s' has the same type as another procedure in the procedure group '%.*s'", LIT(name), LIT(proc_group_name)); // is_invalid = true; // break; case ProcOverload_ParamVariadic: - error(p->token, "Overloaded procedure '%.*s' has the same type as another procedure in the procedure group '%.*s'", LIT(name), LIT(proc_group_name)); is_invalid = true; break; case ProcOverload_ResultCount: case ProcOverload_ResultTypes: - error(p->token, "Overloaded procedure '%.*s' has the same parameters but different results in the procedure group '%.*s'", LIT(name), LIT(proc_group_name)); is_invalid = true; + different_results = true; break; case ProcOverload_Polymorphic: break; @@ -2011,6 +2006,13 @@ gb_internal void check_proc_group_decl(CheckerContext *ctx, Entity *pg_entity, D } if (is_invalid) { + // NOTE(bill): only now, as the error block is shared by every thread + ERROR_BLOCK(); + if (different_results) { + error(p->token, "Overloaded procedure '%.*s' has the same parameters but different results in the procedure group '%.*s'", LIT(name), LIT(proc_group_name)); + } else { + error(p->token, "Overloaded procedure '%.*s' has the same type as another procedure in the procedure group '%.*s'", LIT(name), LIT(proc_group_name)); + } error_line("\tprevious procedure at %s\n", token_pos_to_string(pos)); invalid[k] = true; } @@ -2177,16 +2179,25 @@ gb_internal void check_entity_decl(CheckerContext *ctx, Entity *e, DeclInfo *d, return; } defer (global_when_trial_end_entity(&trial_scope)); - bool is_lazy = (e->flags & EntityFlag_Lazy) != 0; - if (is_lazy) { - mutex_lock(&ctx->info->lazy_mutex); - if (e->state == EntityState_Resolved) { - // NOTE: another thread checked it whilst this one waited - mutex_unlock(&ctx->info->lazy_mutex); + + // NOTE(bill): checked by whichever thread claims it first; any other that needs it meanwhile waits for it + i32 owner = 0; + if (!e->checking_thread.compare_exchange_strong(owner, cast(i32)current_thread_index() + 1)) { + if (thread_wait_for_owner(&e->checking_thread, owner, owner)) { return; } - lazy_mutex_depth += 1; + // NOTE: this thread is checking it already, or the thread checking it waits for this one + error(e->token, "Illegal declaration cycle of `%.*s`", LIT(e->token.string)); + return; } + if (e->state == EntityState_Resolved) { + // NOTE: another thread finished it before this one claimed it + e->checking_thread.store(0); + futex_broadcast(&e->checking_thread); + return; + } + + bool is_lazy = (e->flags & EntityFlag_Lazy) != 0; GlobalEntityTimingFrame timing_frame = global_entity_timing_begin(e); String name = e->token.string; @@ -2290,20 +2301,20 @@ end:; global_entity_timing_end(timing_frame, e); // NOTE(bill): Add it to the list of checked entities if (is_lazy) { + mutex_lock(&ctx->info->lazy_mutex); array_add(&ctx->info->entities, e); - lazy_mutex_depth -= 1; mutex_unlock(&ctx->info->lazy_mutex); } + e->checking_thread.store(0); + futex_broadcast(&e->checking_thread); } -// A lazy entity is only ever in progress on the thread holding `lazy_mutex`, so taking it (it is -// recursive) waits out another thread and is a no-op on the thread that is checking it -gb_internal void wait_for_lazy_entity(CheckerContext *ctx, Entity *e) { - if ((e->flags & EntityFlag_Lazy) == 0 || e->state == EntityState_Resolved) { - return; +// An entity in progress on another thread is waited for, unless that thread waits for this one +gb_internal void wait_for_entity(Entity *e) { + i32 owner = e->checking_thread.load(); + if (owner != 0) { + thread_wait_for_owner(&e->checking_thread, owner, owner); } - mutex_lock(&ctx->info->lazy_mutex); - mutex_unlock(&ctx->info->lazy_mutex); } diff --git a/src/check_expr.cpp b/src/check_expr.cpp index f655b206e..50b59f0c1 100644 --- a/src/check_expr.cpp +++ b/src/check_expr.cpp @@ -955,7 +955,7 @@ gb_internal i64 check_distance_between_types(CheckerContext *c, Operand *operand } if (is_type_union(dst) && allow_unions) { - wait_signal_until_available(&dst->Union.variants_wait_signal); + wait_for_record_signal(&dst->Union.variants_wait_signal, &dst->Union.checking_thread); for (Type *vt : dst->Union.variants) { if (are_types_identical(vt, s)) { return 1; @@ -1713,7 +1713,7 @@ gb_internal Entity *check_ident(CheckerContext *c, Operand *o, Ast *n, Type *nam if (e->state == EntityState_Unresolved) { check_entity_decl(c, e, nullptr, named_type); } else { - wait_for_lazy_entity(c, e); + wait_for_entity(e); } switch (e->kind) { case Entity_Constant: @@ -5172,7 +5172,7 @@ gb_internal void convert_to_typed(CheckerContext *c, Operand *operand, Type *tar case Type_Union: if (!is_operand_nil(*operand) && !is_operand_uninit(*operand)) { TEMPORARY_ALLOCATOR_GUARD(); - wait_signal_until_available(&t->Union.variants_wait_signal); + wait_for_record_signal(&t->Union.variants_wait_signal, &t->Union.checking_thread); isize count = t->Union.variants.count; ValidIndexAndScore *valids = temporary_alloc_array(count); @@ -5205,9 +5205,6 @@ gb_internal void convert_to_typed(CheckerContext *c, Operand *operand, Type *tar first_success_index = valids[0].index; } - gbString type_str = type_to_string(target_type); - defer (gb_string_free(type_str)); - if (valid_count == 1) { Type *new_type = t->Union.variants[first_success_index]; if (operand->mode != Addressing_Constant || @@ -5219,6 +5216,8 @@ gb_internal void convert_to_typed(CheckerContext *c, Operand *operand, Type *tar break; } else if (valid_count > 1) { ERROR_BLOCK(); + gbString type_str = type_to_string(target_type); + defer (gb_string_free(type_str)); GB_ASSERT(first_success_index >= 0); convert_untyped_error(c, operand, target_type, true); @@ -5244,6 +5243,8 @@ gb_internal void convert_to_typed(CheckerContext *c, Operand *operand, Type *tar target_type = t_untyped_uninit; } else if (!is_type_untyped_nil(operand->type) || !type_has_nil(target_type)) { ERROR_BLOCK(); + gbString type_str = type_to_string(target_type); + defer (gb_string_free(type_str)); convert_untyped_error(c, operand, target_type, true); if (count > 0) { @@ -5746,7 +5747,7 @@ gb_internal Entity *check_entity_from_ident_or_selector(CheckerContext *c, Ast * } if (e != nullptr) { // its kind and type are read by the caller - wait_for_lazy_entity(c, e); + wait_for_entity(e); } return e; } else if (!ident_only) if (node->kind == Ast_SelectorExpr) { @@ -8834,9 +8835,7 @@ gb_internal CallArgumentError check_polymorphic_record_type(CheckerContext *c, O set_base_type(named_type, struct_type); check_open_scope(&ctx, node); - begin_filling_record(struct_type); check_struct_type(&ctx, struct_type, node, &ordered_operands, named_type, original_type, found_gen_types); - end_filling_record(struct_type); // check_struct_type released found_gen_types->mutex after publishing the instantiation. gen_types_locked = false; check_close_scope(&ctx); @@ -9745,7 +9744,7 @@ gb_internal bool attempt_implicit_selector_expr(CheckerContext *c, Operand *o, A TEMPORARY_ALLOCATOR_GUARD(); Type *union_type = base_type(th); - wait_signal_until_available(&union_type->Union.variants_wait_signal); + wait_for_record_signal(&union_type->Union.variants_wait_signal, &union_type->Union.checking_thread); auto operands = array_make(temporary_allocator(), 0, union_type->Union.variants.count); for (Type *vt : union_type->Union.variants) { @@ -11088,7 +11087,7 @@ gb_internal ExprKind check_compound_literal(CheckerContext *c, Operand *o, Ast * // If exactly one matches, retarget to that variant and let the normal path build it (the surrounding assignment then wraps it into the union). // Otherwise report a clear error. if (t->kind == Type_Union && cl->type == nullptr && cl->elems.count > 0) { - wait_signal_until_available(&t->Union.variants_wait_signal); + wait_for_record_signal(&t->Union.variants_wait_signal, &t->Union.checking_thread); auto matches = array_make(temporary_allocator(), 0, t->Union.variants.count); for (Type *variant : t->Union.variants) { Operand trial = {}; @@ -11156,7 +11155,7 @@ gb_internal ExprKind check_compound_literal(CheckerContext *c, Operand *o, Ast * break; } - wait_signal_until_available(&t->Struct.fields_wait_signal); + wait_for_record_signal(&t->Struct.fields_wait_signal, &t->Struct.checking_thread); isize field_count = t->Struct.fields.count; isize min_field_count = t->Struct.fields.count; for (isize i = min_field_count-1; i >= 0; i--) { @@ -12105,7 +12104,7 @@ gb_internal ExprKind check_type_assertion(CheckerContext *c, Operand *o, Ast *no Type *src = type_deref(o->type); Type *bsrc = base_type(src); if (bsrc->kind == Type_Union) { - wait_signal_until_available(&bsrc->Union.variants_wait_signal); + wait_for_record_signal(&bsrc->Union.variants_wait_signal, &bsrc->Union.checking_thread); } diff --git a/src/check_stmt.cpp b/src/check_stmt.cpp index dfb046da3..1a8d90a1e 100644 --- a/src/check_stmt.cpp +++ b/src/check_stmt.cpp @@ -849,7 +849,7 @@ gb_internal bool check_using_stmt_entity(CheckerContext *ctx, AstUsingStmt *us, bool is_ptr = is_type_pointer(e->type); Type *t = base_type(type_deref(e->type)); if (t->kind == Type_Struct) { - wait_signal_until_available(&t->Struct.fields_wait_signal); + wait_for_record_signal(&t->Struct.fields_wait_signal, &t->Struct.checking_thread); Scope *found = t->Struct.scope; GB_ASSERT(found != nullptr); @@ -1540,7 +1540,7 @@ gb_internal void check_type_switch_stmt(CheckerContext *ctx, Ast *node, u32 mod_ // TODO(bill): Make robust Type *bt = base_type(type_deref(x.type)); if (bt->kind == Type_Union) { - wait_signal_until_available(&bt->Union.variants_wait_signal); + wait_for_record_signal(&bt->Union.variants_wait_signal, &bt->Union.checking_thread); } Type *case_type = nullptr; diff --git a/src/check_type.cpp b/src/check_type.cpp index 247f52ea3..b7b525702 100644 --- a/src/check_type.cpp +++ b/src/check_type.cpp @@ -724,6 +724,9 @@ gb_internal void check_struct_type(CheckerContext *ctx, Type *struct_type, Ast * GB_ASSERT(is_type_struct(struct_type)); ast_node(st, StructType, node); + struct_type->Struct.checking_thread.store(cast(i32)current_thread_index() + 1); + defer (struct_type->Struct.checking_thread.store(0)); + String context = str_lit("struct"); isize min_field_count = 0; @@ -846,6 +849,9 @@ gb_internal void check_union_type(CheckerContext *ctx, Type *union_type, Ast *no GB_ASSERT(is_type_union(union_type)); ast_node(ut, UnionType, node); + union_type->Union.checking_thread.store(cast(i32)current_thread_index() + 1); + defer (union_type->Union.checking_thread.store(0)); + union_type->Union.node = node; union_type->Union.scope = ctx->scope; @@ -2254,8 +2260,8 @@ gb_internal SubstResult subst_unify(CheckerContext *c, Type *pattern, Type *sour pattern->Struct.is_packed != source->Struct.is_packed) { return Subst_Unhandled; } - wait_signal_until_available(&pattern->Struct.fields_wait_signal); - wait_signal_until_available(&source->Struct.fields_wait_signal); + wait_for_record_signal(&pattern->Struct.fields_wait_signal, &pattern->Struct.checking_thread); + wait_for_record_signal(&source->Struct.fields_wait_signal, &source->Struct.checking_thread); if (pattern->Struct.fields.count != source->Struct.fields.count) { return Subst_NoMatch; } @@ -4391,7 +4397,7 @@ gb_internal bool complete_soa_type(Checker *checker, Type *t, bool wait_to_finis GB_ASSERT(old_struct->kind == Type_Struct); if (wait_to_finish) { - wait_signal_until_available(&old_struct->Struct.fields_wait_signal); + wait_for_record_signal(&old_struct->Struct.fields_wait_signal, &old_struct->Struct.checking_thread); } else { GB_ASSERT(old_struct->Struct.fields_wait_signal.futex.load() != 0); } diff --git a/src/checker.cpp b/src/checker.cpp index 95961c8a1..ff59665ec 100644 --- a/src/checker.cpp +++ b/src/checker.cpp @@ -7537,8 +7537,12 @@ gb_internal void check_parsed_files(Checker *c) { array_sort(c->info.entities, init_procedures_cmp); TIME_SECTION("check all global entities"); + isize entity_count = c->info.entities.count; check_all_global_entities(c); + // NOTE(bill): lazy entities are added once checked, which with several threads is in no fixed order + gb_sort_array(c->info.entities.data + entity_count, c->info.entities.count - entity_count, init_procedures_cmp); + if (build_context.internal_global_entity_graph) { TIME_SECTION("print global entity graph"); print_global_groups(&global_groups); diff --git a/src/checker.hpp b/src/checker.hpp index 78f579c2d..52d43ad81 100644 --- a/src/checker.hpp +++ b/src/checker.hpp @@ -780,7 +780,7 @@ struct CheckerInfo { BlockingMutex type_and_value_mutex; - RecursiveMutex lazy_mutex; // Mutex required for lazy type checking of specific files + RecursiveMutex lazy_mutex; // for adding checked lazy entities to `entities` // BlockingMutex type_info_mutex; // NOT recursive @@ -970,7 +970,7 @@ struct GlobalEntityTimingFrame { }; gb_internal GlobalEntityTimingFrame global_entity_timing_begin(Entity *e); gb_internal void global_entity_timing_end(GlobalEntityTimingFrame const &f, Entity *e); -gb_internal void wait_for_lazy_entity(CheckerContext *c, Entity *e); +gb_internal void wait_for_entity(Entity *e); gb_internal Ast *remove_type_alias_clutter(Ast *node); gb_internal void check_const_decl(CheckerContext *c, Entity *e, Ast *type_expr, Ast *init_expr, Type *named_type); gb_internal void check_type_decl(CheckerContext *c, Entity *e, Ast *type_expr, Type *def); diff --git a/src/checker_global.cpp b/src/checker_global.cpp index fd8b51130..2ca4e0e27 100644 --- a/src/checker_global.cpp +++ b/src/checker_global.cpp @@ -749,27 +749,32 @@ gb_internal void global_graph_print_entity(Entity *e) { struct GlobalGroup { - i32 start; // into `GlobalGroupGraph::members` - i32 count; - bool done; + i32 start; // into `GlobalGroupGraph::members` + i32 count; + std::atomic done; }; struct GlobalGroupGraph { Array nodes; PtrMap node_of; - Array offsets; // node -> the nodes it names, as `targets[offsets[v]..offsets[v+1]]` + Array offsets; // node -> the nodes it names, as `targets[offsets[v].. targets; Array group_of; - Array groups; // every dependency of a group has a lower index - Array members; // nodes, by group, in source order + Array groups; // every dependency of a group has a lower index + Array members; // nodes by group in source order - bool active; - i32 current_group; - Entity *current_entity; - isize missing_edges; + Array dependent_offsets; // group -> the groups that depend on it as `dependents[dependent_offsets[gi].. dependents; + std::atomic * pending; // per group its dependencies not yet done + + Checker * checker; + bool active; + std::atomic missing_edges; }; gb_global GlobalGroupGraph global_groups; +gb_global gb_thread_local i32 global_group_current = -1; +gb_global gb_thread_local Entity * global_group_current_entity; struct GlobalPlaceholderHit { Scope * scope; @@ -1268,7 +1273,9 @@ gb_internal void build_global_groups(Checker *c, GlobalGroupGraph *g) { array_init(&g->groups, heap_allocator(), group_count); array_init(&g->members, heap_allocator(), node_count); for (i32 gi = 0; gi < group_count; gi++) { - g->groups[gi] = {}; + g->groups[gi].start = 0; + g->groups[gi].count = 0; + g->groups[gi].done.store(false); } for (i32 v = 0; v < node_count; v++) { g->groups[g->group_of[v]].count += 1; @@ -1299,7 +1306,7 @@ gb_internal void global_group_check_edge(CheckerContext *ctx, Entity *e) { } } else { i32 gi = g->group_of[*v]; - if (gi == g->current_group || g->groups[gi].done) { + if (gi == global_group_current || g->groups[gi].done.load()) { return; } } @@ -1312,9 +1319,9 @@ gb_internal void global_group_check_edge(CheckerContext *ctx, Entity *e) { gb_printf_err(" needs "); global_graph_print_entity(e); gb_printf_err(v == nullptr ? ", which is not in the graph" : ""); - if (g->current_entity != by) { + if (global_group_current_entity != by) { gb_printf_err(", while checking "); - global_graph_print_entity(g->current_entity); + global_graph_print_entity(global_group_current_entity); } gb_printf_err("\n"); } @@ -1331,14 +1338,14 @@ gb_internal void check_global_group(Checker *c, GlobalGroupGraph *g, i32 gi) { auto soa_types = array_make(heap_allocator()); global_group_soa_types = &soa_types; - g->current_group = gi; + global_group_current = gi; for (i32 k = 0; k < group->count; k++) { Entity *e = g->nodes[members[k]]; if (e->flags & EntityFlag_Lazy) { // NOTE: only checked when something uses it; the group orders it after what it names continue; } - g->current_entity = e; + global_group_current_entity = e; GlobalEntityTimingFrame timing_frame = global_entity_timing_begin(e); check_single_global_entity(c, e, e->decl_info, &untyped); if (e->type != nullptr && is_type_typed(e->type)) { @@ -1361,41 +1368,24 @@ gb_internal void check_global_group(Checker *c, GlobalGroupGraph *g, i32 gi) { add_untyped_expressions(&c->info, &untyped); map_destroy(&untyped); - group->done = true; - g->current_group = -1; - g->current_entity = nullptr; + group->done.store(true); + global_group_current = -1; + global_group_current_entity = nullptr; } -// Groups in dependency order; with `-internal-shuffle-global-entities`, a random one of the groups whose -// dependencies are done, as a parallel checker might -gb_internal void check_global_groups(Checker *c, GlobalGroupGraph *g) { +gb_internal void build_global_group_dependents(GlobalGroupGraph *g) { i32 group_count = cast(i32)g->groups.count; - u64 seed = build_context.internal_shuffle_global_entities; - if (seed == 0) { - for (i32 gi = 0; gi < group_count; gi++) { - check_global_group(c, g, gi); - } - return; - } - - auto dependents = array_make >(heap_allocator(), group_count); - auto dep_count = array_make (heap_allocator(), group_count); - auto seen = array_make (heap_allocator(), group_count); - auto ready = array_make (heap_allocator(), 0, group_count); - defer ({ - for (auto &d : dependents) { - array_free(&d); - } - array_free(&dependents); - }); - defer (array_free(&dep_count)); + auto edge_from = array_make(heap_allocator(), 0, group_count); + auto edge_to = array_make(heap_allocator(), 0, group_count); + auto seen = array_make(heap_allocator(), group_count); + defer (array_free(&edge_from)); + defer (array_free(&edge_to)); defer (array_free(&seen)); - defer (array_free(&ready)); + g->pending = gb_alloc_array(heap_allocator(), std::atomic, group_count); for (i32 gi = 0; gi < group_count; gi++) { - dep_count[gi] = 0; - dependents[gi] = {}; seen[gi] = -1; + g->pending[gi].store(0); } for (i32 gi = 0; gi < group_count; gi++) { GlobalGroup const &group = g->groups[gi]; @@ -1405,36 +1395,79 @@ gb_internal void check_global_groups(Checker *c, GlobalGroupGraph *g) { i32 dep = g->group_of[g->targets[i]]; if (dep != gi && seen[dep] != gi) { seen[dep] = gi; - dep_count[gi] += 1; - if (dependents[dep].allocator.proc == nullptr) { - array_init(&dependents[dep], heap_allocator()); - } - array_add(&dependents[dep], gi); + array_add(&edge_from, dep); + array_add(&edge_to, gi); + g->pending[gi].fetch_add(1); } } } - if (dep_count[gi] == 0) { + } + global_graph_csr(group_count, edge_from, edge_to, &g->dependent_offsets, &g->dependents); +} + +gb_internal void check_global_group_and_release(GlobalGroupGraph *g, i32 gi, Array *ready); + +gb_internal WORKER_TASK_PROC(check_global_group_worker) { + check_global_group_and_release(&global_groups, cast(i32)cast(intptr)data, nullptr); + return 0; +} + +gb_internal void check_global_group_and_release(GlobalGroupGraph *g, i32 gi, Array *ready) { + check_global_group(g->checker, g, gi); + for (i32 i = g->dependent_offsets[gi]; i < g->dependent_offsets[gi+1]; i++) { + i32 next = g->dependents[i]; + if (g->pending[next].fetch_sub(1) == 1) { + if (ready != nullptr) { + array_add(ready, next); + } else { + thread_pool_add_task(check_global_group_worker, cast(void *)cast(intptr)next); + } + } + } +} + +gb_internal void check_global_groups(Checker *c, GlobalGroupGraph *g) { + i32 group_count = cast(i32)g->groups.count; + u64 seed = build_context.internal_shuffle_global_entities; + g->checker = c; + + if (seed == 0 && (build_context.thread_count <= 1 || build_context.no_threaded_checker)) { + for (i32 gi = 0; gi < group_count; gi++) { + check_global_group(c, g, gi); + } + return; + } + + build_global_group_dependents(g); + + // NOTE: all found before any is checked, as checking one releases others + auto ready = array_make(heap_allocator(), 0, group_count); + defer (array_free(&ready)); + for (i32 gi = 0; gi < group_count; gi++) { + if (g->pending[gi].load() == 0) { array_add(&ready, gi); } } - u64 state = seed; - isize checked = 0; - while (ready.count > 0) { - isize i = cast(isize)(global_group_random(&state) % cast(u64)ready.count); - i32 gi = ready[i]; - ready[i] = ready[ready.count-1]; - array_pop(&ready); - - check_global_group(c, g, gi); - checked += 1; - for (i32 next : dependents[gi]) { - if (--dep_count[next] == 0) { - array_add(&ready, next); - } + if (seed == 0) { + for (i32 gi : ready) { + thread_pool_add_task(check_global_group_worker, cast(void *)cast(intptr)gi); + } + thread_pool_wait(); + } else { + u64 state = seed; + while (ready.count > 0) { + isize i = cast(isize)(global_group_random(&state) % cast(u64)ready.count); + i32 gi = ready[i]; + ready[i] = ready[ready.count-1]; + array_pop(&ready); + check_global_group_and_release(g, gi, &ready); } } - GB_ASSERT(checked == group_count); + + for (i32 gi = 0; gi < group_count; gi++) { + GB_ASSERT(g->groups[gi].done.load()); + } } gb_internal void destroy_global_groups(GlobalGroupGraph *g) { @@ -1445,6 +1478,12 @@ gb_internal void destroy_global_groups(GlobalGroupGraph *g) { array_free(&g->group_of); array_free(&g->groups); array_free(&g->members); + array_free(&g->dependent_offsets); + array_free(&g->dependents); + if (g->pending != nullptr) { + gb_free(heap_allocator(), g->pending); + g->pending = nullptr; + } } gb_internal void check_all_global_entities(Checker *c) { @@ -1470,12 +1509,11 @@ gb_internal void check_all_global_entities(Checker *c) { TIME_SECTION("check all global entities - check groups"); g->active = true; - g->current_group = -1; check_global_groups(c, g); g->active = false; - if (build_context.internal_check_global_edges && g->missing_edges > 0) { - gb_printf_err("%td missing global dependencies\n", g->missing_edges); + if (build_context.internal_check_global_edges && g->missing_edges.load() > 0) { + gb_printf_err("%td missing global dependencies\n", g->missing_edges.load()); gb_exit(1); } @@ -2217,7 +2255,7 @@ gb_internal void print_global_groups(GlobalGroupGraph *g) { f64 critical_ms = critical >= 0 ? global_graph_ms(path[critical], freq) : 0; gb_printf_err("Global entity groups\n"); - gb_printf_err(" entities: %td (%td not timed), dependency edges: %td, missing edges: %td\n", g->nodes.count, untimed, g->targets.count, g->missing_edges); + gb_printf_err(" entities: %td (%td not timed), dependency edges: %td, missing edges: %td\n", g->nodes.count, untimed, g->targets.count, g->missing_edges.load()); gb_printf_err(" self time: %.3f ms in the groups, %.3f ms during 'when' resolution\n", total_ms, global_graph_ms(when_ticks, freq)); gb_printf_err(" groups: %d (%td with a cycle), largest has %d entities\n", group_count, cyclic, largest >= 0 ? g->groups[largest].count : 0); gb_printf_err(" critical path: %.3f ms over %d groups -> at most %.2fx speedup\n", diff --git a/src/entity.cpp b/src/entity.cpp index 845e2b910..4a7800450 100644 --- a/src/entity.cpp +++ b/src/entity.cpp @@ -210,6 +210,7 @@ struct Entity { u64 id; std::atomic flags; std::atomic state; + Futex checking_thread; // 1 + the index of the thread in `check_entity_decl` for it, else 0 std::atomic min_dep_count; Token token; Scope * scope; diff --git a/src/thread_pool.cpp b/src/thread_pool.cpp index 0ffce8046..b62811a5a 100644 --- a/src/thread_pool.cpp +++ b/src/thread_pool.cpp @@ -174,6 +174,47 @@ gb_internal bool thread_pool_add_task(ThreadPool *pool, WorkerTaskProc *proc, vo return true; } +gb_internal bool thread_wait_for_owner(Futex *futex, Footex value, i32 owner) { + if (futex->load() != value) { + return true; + } + Thread *self = current_thread; + if (self != nullptr && owner > 0) { + i32 me = cast(i32)self->idx + 1; + if (owner == me) { + return false; + } + self->waiting_futex.store(futex); + self->waiting_value.store(value); + self->waiting_for.store(owner); + + // NOTE(bill): a thread counts as waiting only while what it waits on is unchanged + // when it clears its own `waiting_for` only once it has woken + Slice threads = self->pool->threads; + i32 t = owner; + for (isize i = 0; i <= threads.count; i++) { + if (t == me) { + self->waiting_for.store(0); + return false; + } + Thread *other = &threads[t-1]; + i32 next = other->waiting_for.load(); + Futex *f = other->waiting_futex.load(); + if (next == 0 || f == nullptr || f->load() != other->waiting_value.load()) { + break; + } + t = next; + } + } + while (futex->load() == value) { + futex_wait(futex, value); + } + if (self != nullptr) { + self->waiting_for.store(0); + } + return true; +} + gb_internal void thread_pool_wait(ThreadPool *pool) { WorkerTask task; diff --git a/src/threading.cpp b/src/threading.cpp index 524c70e29..28882ea24 100644 --- a/src/threading.cpp +++ b/src/threading.cpp @@ -73,6 +73,10 @@ struct Thread { struct Arena *permanent_arena; struct Arena *temporary_arena; + + std::atomic *> waiting_futex; + std::atomic waiting_value; + std::atomic waiting_for; }; typedef std::atomic Futex; diff --git a/src/types.cpp b/src/types.cpp index aad687ac7..129a5afd1 100644 --- a/src/types.cpp +++ b/src/types.cpp @@ -155,6 +155,7 @@ struct TypeStruct { i32 soa_count; StructSoaKind soa_kind; Wait_Signal fields_wait_signal; + Futex checking_thread; // 1 + the index of the thread in `check_struct_type` for it, else 0 BlockingMutex soa_mutex; BlockingMutex offset_mutex; // for settings offsets @@ -181,6 +182,7 @@ struct TypeUnion { Type * polymorphic_parent; Wait_Signal polymorphic_wait_signal; Wait_Signal variants_wait_signal; // signalled once `variants` is populated (mirrors TypeStruct.fields_wait_signal) + Futex checking_thread; // 1 + the index of the thread in `check_union_type` for it, else 0 std::atomic tag_size; bool is_polymorphic; @@ -413,6 +415,7 @@ gb_internal bool is_type_simple_compare(Type *t); gb_internal Type *type_deref(Type *t, bool allow_multi_pointer=false); gb_internal Type *base_type(Type *t); gb_internal Type *alloc_type_multi_pointer(Type *elem); +gb_internal void wait_for_record_signal(Wait_Signal *signal, Futex *checking_thread); gb_internal u32 type_info_flags_of_type(Type *type) { if (type == nullptr) { @@ -2477,13 +2480,13 @@ gb_internal TypeTuple *get_record_polymorphic_params(Type *t) { t = base_type(t); switch (t->kind) { case Type_Struct: - wait_signal_until_available(&t->Struct.polymorphic_wait_signal); + wait_for_record_signal(&t->Struct.polymorphic_wait_signal, &t->Struct.checking_thread); if (t->Struct.polymorphic_params) { return &t->Struct.polymorphic_params->Tuple; } break; case Type_Union: - wait_signal_until_available(&t->Union.polymorphic_wait_signal); + wait_for_record_signal(&t->Union.polymorphic_wait_signal, &t->Union.checking_thread); if (t->Union.polymorphic_params) { return &t->Union.polymorphic_params->Tuple; } @@ -3560,7 +3563,7 @@ gb_internal bool union_variant_index_types_equal(Type *v, Type *vt) { gb_internal i64 union_variant_index_checked(Type *u, Type *v) { u = base_type(u); GB_ASSERT(u->kind == Type_Union); - wait_signal_until_available(&u->Union.variants_wait_signal); + wait_for_record_signal(&u->Union.variants_wait_signal, &u->Union.checking_thread); for_array(i, u->Union.variants) { Type *vt = u->Union.variants[i]; @@ -3579,7 +3582,7 @@ gb_internal i64 union_variant_index_checked(Type *u, Type *v) { gb_internal bool union_is_variant_of(Type *u, Type *v) { u = base_type(u); GB_ASSERT(u->kind == Type_Union); - wait_signal_until_available(&u->Union.variants_wait_signal); + wait_for_record_signal(&u->Union.variants_wait_signal, &u->Union.checking_thread); for_array(i, u->Union.variants) { Type *vt = u->Union.variants[i]; @@ -3770,7 +3773,7 @@ gb_internal Selection lookup_field_from_index(Type *type, i64 index) { isize max_count = 0; switch (type->kind) { case Type_Struct: - wait_signal_until_available(&type->Struct.fields_wait_signal); + wait_for_record_signal(&type->Struct.fields_wait_signal, &type->Struct.checking_thread); max_count = type->Struct.fields.count; break; case Type_Tuple: max_count = type->Tuple.variables.count; break; @@ -3782,7 +3785,7 @@ gb_internal Selection lookup_field_from_index(Type *type, i64 index) { switch (type->kind) { case Type_Struct: { - wait_signal_until_available(&type->Struct.fields_wait_signal); + wait_for_record_signal(&type->Struct.fields_wait_signal, &type->Struct.checking_thread); for (isize i = 0; i < max_count; i++) { Entity *f = type->Struct.fields[i]; if (f->kind == Entity_Variable) { @@ -3951,7 +3954,7 @@ gb_internal Selection lookup_field_with_selection(Type *type_, InternedString fi // NOTE(bill): A polymorphic struct has no fields, this only hits in the case of an error return sel; } - wait_signal_until_available(&type->Struct.fields_wait_signal); + wait_for_record_signal(&type->Struct.fields_wait_signal, &type->Struct.checking_thread); isize field_count = type->Struct.fields.count; if (field_count != 0) for_array(i, type->Struct.fields) { Entity *f = type->Struct.fields[i]; @@ -4421,33 +4424,16 @@ gb_internal i64 type_target_max_align(void) { return max_align; } -// Polymorphic record instances being filled by this thread, which another may already have found in the cache -gb_internal gb_thread_local Array records_being_filled; - -gb_internal void begin_filling_record(Type *t) { - if (records_being_filled.allocator.proc == nullptr) { - array_init(&records_being_filled, heap_allocator()); +gb_internal void wait_for_record_signal(Wait_Signal *signal, Futex *checking_thread) { + if (signal->futex.load() == 0) { + thread_wait_for_owner(&signal->futex, 0, checking_thread->load()); } - array_add(&records_being_filled, t); } -gb_internal void end_filling_record(Type *t) { - GB_ASSERT(records_being_filled.count > 0 && records_being_filled[records_being_filled.count-1] == t); - array_pop(&records_being_filled); -} - -gb_internal gb_thread_local i32 lazy_mutex_depth; - gb_internal void wait_for_struct_fields(Type *t) { - if (t->Struct.polymorphic_parent == nullptr || t->Struct.fields_wait_signal.futex.load() != 0 || lazy_mutex_depth > 0) { - return; + if (t->Struct.polymorphic_parent != nullptr) { + wait_for_record_signal(&t->Struct.fields_wait_signal, &t->Struct.checking_thread); } - for (Type *r : records_being_filled) { - if (r == t) { - return; - } - } - wait_signal_until_available(&t->Struct.fields_wait_signal); } gb_internal i64 type_align_of_internal(Type *t, TypePath *path) { diff --git a/tests/issues/run.bat b/tests/issues/run.bat index b5b90339c..28fee09f4 100644 --- a/tests/issues/run.bat +++ b/tests/issues/run.bat @@ -61,6 +61,7 @@ set COMMON=-define:ODIN_TEST_FANCY=false -file -vet -strict-style -ignore-unused ..\..\..\odin check ..\test_issue_7336.odin -no-entry-point %COMMON% || exit /b ..\..\..\odin check ..\test_issue_ellipsis_type_call.odin -no-entry-point %COMMON% 2>&1 | find /c "Error:" | findstr /x "10" || exit /b ..\..\..\odin check ..\test_issue_foreign_redeclaration.odin -no-entry-point %COMMON% || exit /b +..\..\..\odin check ..\test_issue_foreign_import_attributes.odin -no-entry-point %COMMON% || exit /b ..\..\..\odin check ..\test_issue_foreign_redeclaration_mismatch.odin -no-entry-point %COMMON% 2>&1 | find /c "Error:" | findstr /x "1" || exit /b ..\..\..\odin doc ..\test_issue_asm_doc_category.odin -file 2>&1 | find /c "asm templates" | findstr /x "1" || exit /b ..\..\..\odin build ..\test_issue_7037.odin %COMMON% -o:none || exit /b @@ -77,6 +78,7 @@ clang -c ..\test_issue_sysv_abi.c -o test_issue_sysv_abi_c.o || exit /b ..\..\..\odin run ..\test_issue_7482.odin %COMMON% || exit /b ..\..\..\odin run ..\test_issue_7562.odin %COMMON% -no-crt -no-thread-local || exit /b ..\..\..\odin run ..\test_issue_7562.odin %COMMON% -no-crt -no-thread-local -o:speed || exit /b +..\..\..\odin test ..\test_issue_7566.odin %COMMON% || exit /b ..\..\..\odin test ..\test_issue_7587.odin %COMMON% || exit /b ..\..\..\odin run ..\test_issue_7596.odin %COMMON% || exit /b diff --git a/tests/issues/run.sh b/tests/issues/run.sh index 8bc063be0..209d2923d 100755 --- a/tests/issues/run.sh +++ b/tests/issues/run.sh @@ -101,6 +101,7 @@ $ODIN check ../test_issue_7012.odin -no-entry-point $COMMON_CHECK $ODIN build ../test_issue_7037.odin $COMMON -o:none $ODIN run ../test_issue_7482.odin $COMMON $ODIN run ../test_issue_7564.odin $COMMON +$ODIN test ../test_issue_7566.odin $COMMON $ODIN test ../test_issue_7587.odin $COMMON $ODIN run ../test_issue_7596.odin $COMMON $ODIN test ../test_issue_7421.odin $COMMON @@ -151,6 +152,7 @@ else fi $ODIN check ../test_issue_foreign_redeclaration.odin -no-entry-point $COMMON_CHECK +$ODIN check ../test_issue_foreign_import_attributes.odin -no-entry-point $COMMON_CHECK if [[ $($ODIN check ../test_issue_foreign_redeclaration_mismatch.odin -no-entry-point $COMMON_CHECK 2>&1 >/dev/null | grep -c "Error:") -eq 1 ]]; then echo "SUCCESSFUL 1/1" else diff --git a/tests/issues/test_issue_7566.odin b/tests/issues/test_issue_7566.odin new file mode 100644 index 000000000..3bbef41e7 --- /dev/null +++ b/tests/issues/test_issue_7566.odin @@ -0,0 +1,36 @@ +// Tests issue #7566 https://github.com/odin-lang/Odin/issues/7566 +// Polymorphic instances whose constant parameters are spelt the same but have different values +package test_issues + +import "core:testing" + +Dim :: enum{ C, F } +Dims :: [Dim]int +Arr2 :: [2][2]int + +get_dims :: proc($dims: Dims) -> Dims { return dims } +get_arr2 :: proc($a: Arr2) -> Arr2 { return a } + +make_and_check :: proc($A, $I: int) -> Dims { + dims :: Dims{.C = I, .F = A} + return get_dims(dims) +} + +make_inline :: proc($A, $I: int) -> Dims { + return get_dims(Dims{.C = I, .F = A}) +} + +make_nested :: proc($A: int) -> Arr2 { + a :: Arr2{{A, 1}, {2, A}} + return get_arr2(a) +} + +@(test) +test_issue_7566 :: proc(t: ^testing.T) { + testing.expect_value(t, make_and_check(2, 2), Dims{.C = 2, .F = 2}) + testing.expect_value(t, make_and_check(3, 3), Dims{.C = 3, .F = 3}) + testing.expect_value(t, make_inline(4, 5), Dims{.C = 5, .F = 4}) + testing.expect_value(t, make_inline(6, 7), Dims{.C = 7, .F = 6}) + testing.expect_value(t, make_nested(8), Arr2{{8, 1}, {2, 8}}) + testing.expect_value(t, make_nested(9), Arr2{{9, 1}, {2, 9}}) +} diff --git a/tests/issues/test_issue_foreign_import_attributes.odin b/tests/issues/test_issue_foreign_import_attributes.odin new file mode 100644 index 000000000..d7dd1cb80 --- /dev/null +++ b/tests/issues/test_issue_foreign_import_attributes.odin @@ -0,0 +1,13 @@ +// 'foreign import' attributes may name constants declared after them +package test_issues + +@(priority_index=PRIORITY, extra_linker_flags=FLAGS) +foreign import lib "system:foo" + +foreign lib { + foo :: proc "c" () --- +} + +PRIORITY :: LATER + 1 +FLAGS :: "-L." +LATER :: 1 diff --git a/tests/issues/test_issue_global_when_cycle_accepted.odin b/tests/issues/test_issue_global_when_cycle_accepted.odin new file mode 100644 index 000000000..5d4b1e2a1 --- /dev/null +++ b/tests/issues/test_issue_global_when_cycle_accepted.odin @@ -0,0 +1,28 @@ +// Cycles of global 'when's with exactly one consistent choice of branches +package test_issues + +import "core:testing" + +// only the first branch of the second 'when' is consistent, so `int` stays the builtin +when size_of(int) == 8 { Y :: 1 } +when Y == 1 { Z :: 1 } else { int :: i32 } + +// through an unconditional declaration, whose value is checked for each choice +SIZE :: size_of(uint) +when SIZE == 8 { V :: 1 } +when V == 1 { W :: 2 } else { uint :: u32 } + +// nested and 'else when' branches +when size_of(rawptr) == 8 { + when true { N :: 1 } +} +when N != 1 { rawptr :: u32 } else when N == 1 { M :: 3 } else { rawptr :: u16 } + +@(test) +test_global_when_cycle_accepted :: proc(t: ^testing.T) { + testing.expect_value(t, Z, 1) + testing.expect_value(t, size_of(int), 8) + testing.expect_value(t, SIZE, 8) + testing.expect_value(t, W, 2) + testing.expect_value(t, M, 3) +} diff --git a/tests/issues/test_issue_global_when_cycle_ambiguous.odin b/tests/issues/test_issue_global_when_cycle_ambiguous.odin new file mode 100644 index 000000000..90ebb69db --- /dev/null +++ b/tests/issues/test_issue_global_when_cycle_ambiguous.odin @@ -0,0 +1,5 @@ +// A cycle of global 'when's with two consistent choices of branches, which is an error +package test_issues + +when size_of(int) == 8 { uint :: u32 } +when size_of(uint) == 8 { int :: i32 } From 0de3f92449a1d4bfbd4845ac91e370866ca16f64 Mon Sep 17 00:00:00 2001 From: gingerBill Date: Fri, 2 Oct 2026 11:24:28 +0100 Subject: [PATCH 12/17] Thread pool: wake one sleeping worker per task instead of all, spin briefly before sleeping; walk the procedure dependency tree in chunks --- src/checker.cpp | 39 +++++++++------ src/thread_pool.cpp | 118 +++++++++++++++++++++++++------------------- 2 files changed, 92 insertions(+), 65 deletions(-) diff --git a/src/checker.cpp b/src/checker.cpp index ff59665ec..bd92cad9e 100644 --- a/src/checker.cpp +++ b/src/checker.cpp @@ -7371,8 +7371,6 @@ gb_internal void check_update_dependency_tree_for_procedures(Checker *c) { } } #else -gb_internal void check_walk_all_dependencies(DeclInfo *decl); - // NOTE: post-order, so a declaration has the dependencies of all those nested in it before they are added to its parent gb_internal void check_walk_all_dependencies_post_order(DeclInfo *decl) { for (DeclInfo *child = decl->next_child; child != nullptr; child = child->next_sibling) { @@ -7381,30 +7379,41 @@ gb_internal void check_walk_all_dependencies_post_order(DeclInfo *decl) { add_deps_from_child_to_parent(decl); } +// NOTE(bill): in chunks, as with a task for each entity, adding the tasks was most of the work +struct CheckWalkDependenciesChunk { + DeclInfo **decls; // either these + Entity ** entities; // or the declarations of these + isize count; +}; + gb_internal WORKER_TASK_PROC(check_walk_all_dependencies_worker_proc) { - if (data != nullptr) { - check_walk_all_dependencies_post_order(cast(DeclInfo *)data); + CheckWalkDependenciesChunk *chunk = cast(CheckWalkDependenciesChunk *)data; + for (isize i = 0; i < chunk->count; i++) { + DeclInfo *decl = chunk->decls != nullptr ? chunk->decls[i] : chunk->entities[i]->decl_info; + if (decl != nullptr) { + check_walk_all_dependencies_post_order(decl); + } } return 0; } -gb_internal void check_walk_all_dependencies(DeclInfo *decl) { - if (decl != nullptr) { - thread_pool_add_task(check_walk_all_dependencies_worker_proc, decl); - } -} - gb_internal void check_update_dependency_tree_for_procedures(Checker *c) { + isize const CHUNK_SIZE = 256; + auto chunks = array_make(heap_allocator(), 0, c->info.entities.count/CHUNK_SIZE + 16); + defer (array_free(&chunks)); + mutex_lock(&c->nested_proc_lits_mutex); - for (DeclInfo *decl : c->nested_proc_lits) { - check_walk_all_dependencies(decl); + for (isize i = 0; i < c->nested_proc_lits.count; i += CHUNK_SIZE) { + array_add(&chunks, CheckWalkDependenciesChunk{c->nested_proc_lits.data + i, nullptr, gb_min(CHUNK_SIZE, c->nested_proc_lits.count - i)}); } mutex_unlock(&c->nested_proc_lits_mutex); - for (Entity *e : c->info.entities) { - DeclInfo *decl = e->decl_info; - check_walk_all_dependencies(decl); + for (isize i = 0; i < c->info.entities.count; i += CHUNK_SIZE) { + array_add(&chunks, CheckWalkDependenciesChunk{nullptr, c->info.entities.data + i, gb_min(CHUNK_SIZE, c->info.entities.count - i)}); } + for (CheckWalkDependenciesChunk &chunk : chunks) { + thread_pool_add_task(check_walk_all_dependencies_worker_proc, &chunk); + } thread_pool_wait(); } #endif diff --git a/src/thread_pool.cpp b/src/thread_pool.cpp index b62811a5a..0f1e59c93 100644 --- a/src/thread_pool.cpp +++ b/src/thread_pool.cpp @@ -29,20 +29,20 @@ enum GrabState { Grab_Failed = 2, }; -enum BroadcastWaitState { - Nobody_Waiting = 0, - Someone_Waiting = 1, -}; - struct ThreadPool { gbAllocator threads_allocator; Slice threads; std::atomic running; - Futex tasks_available; - Futex tasks_left; + Futex tasks_available; // bumped to wake a sleeping worker + std::atomic sleeping; // workers asleep on `tasks_available`, or about to be + Futex tasks_left; }; +// NOTE(bill): how many times an idle worker looks for a task before sleeping, so one adding small tasks +// one after another keeps the workers busy rather than waking one for each +enum { THREAD_POOL_SPIN_COUNT = 64 }; + gb_internal isize current_thread_index(void) { return current_thread ? current_thread->idx : 0; } @@ -69,8 +69,8 @@ gb_internal void thread_pool_destroy(ThreadPool *pool) { for_array_off(i, 1, pool->threads) { Thread *t = &pool->threads[i]; - pool->tasks_available.store(Nobody_Waiting); - futex_broadcast(&t->pool->tasks_available); + pool->tasks_available.fetch_add(1); + futex_broadcast(&pool->tasks_available); thread_join_and_destroy(t); } @@ -103,9 +103,13 @@ void thread_pool_queue_push(Thread *thread, WorkerTask task) { thread->queue.bottom.store(bot + 1, std::memory_order_relaxed); thread->pool->tasks_left.fetch_add(1, std::memory_order_release); - i32 state = Someone_Waiting; - if (thread->pool->tasks_available.compare_exchange_strong(state, Nobody_Waiting)) { - futex_broadcast(&thread->pool->tasks_available); + + // NOTE(bill): one sleeping worker per task; waking them all made a loop adding small tasks mostly + // wake-ups, as they were back asleep before the next. The fence pairs with the one in the worker loop. + std::atomic_thread_fence(std::memory_order_seq_cst); + if (thread->pool->sleeping.load(std::memory_order_relaxed) > 0) { + thread->pool->tasks_available.fetch_add(1); + futex_signal(&thread->pool->tasks_available); } } @@ -165,6 +169,40 @@ GrabState thread_pool_queue_steal(Thread *thread, WorkerTask *task) { return ret; } +gb_internal bool thread_pool_queue_has_tasks(Thread *thread) { + return thread->queue.top.load(std::memory_order_acquire) < thread->queue.bottom.load(std::memory_order_acquire); +} + +// Runs a task from another thread's queue; false if none had one +gb_internal bool thread_pool_steal(ThreadPool *pool) { + usize idx = cast(usize)current_thread->idx; + for_array(i, pool->threads) { + idx = (idx + 1) % cast(usize)pool->threads.count; + Thread *thread = &pool->threads.data[idx]; + if (!thread_pool_queue_has_tasks(thread)) { + continue; + } + + WorkerTask task; + switch (thread_pool_queue_steal(thread, &task)) { + case Grab_Empty: + continue; + case Grab_Success: + task.do_work(task.data); + pool->tasks_left.fetch_sub(1, std::memory_order_release); + + if (pool->tasks_left.load(std::memory_order_acquire) == 0) { + futex_signal(&pool->tasks_left); + } + return true; + case Grab_Failed: + // NOTE: another thread took it, so there may be more + return true; + } + } + return false; +} + gb_internal bool thread_pool_add_task(ThreadPool *pool, WorkerTaskProc *proc, void *data) { WorkerTask task = {}; task.do_work = proc; @@ -247,7 +285,6 @@ gb_internal THREAD_PROC(thread_pool_thread_proc) { while (pool->running.load(std::memory_order_seq_cst)) { // If we've got tasks to process, work through them usize finished_tasks = 0; - i32 state; while (!thread_pool_queue_take(current_thread, &task)) { task.do_work(task.data); @@ -260,48 +297,29 @@ gb_internal THREAD_PROC(thread_pool_thread_proc) { } // If there's still work somewhere and we don't have it, steal it - if (pool->tasks_left.load(std::memory_order_acquire)) { - usize idx = cast(usize)current_thread->idx; - for_array(i, pool->threads) { - if (pool->tasks_left.load(std::memory_order_acquire) == 0) { - break; - } - - idx = (idx + 1) % cast(usize)pool->threads.count; - - Thread *thread = &pool->threads.data[idx]; - WorkerTask task; - - GrabState ret = thread_pool_queue_steal(thread, &task); - switch (ret) { - case Grab_Empty: - continue; - case Grab_Success: - task.do_work(task.data); - pool->tasks_left.fetch_sub(1, std::memory_order_release); - - if (pool->tasks_left.load(std::memory_order_acquire) == 0) { - futex_signal(&pool->tasks_left); - } - - /*fallthrough*/ - case Grab_Failed: - goto main_loop_continue; - } + for (isize spin = 0; spin < THREAD_POOL_SPIN_COUNT; spin++) { + if (thread_pool_steal(pool)) { + goto main_loop_continue; } + yield_thread(); } // if we've done all our work, and there's nothing to steal, go to sleep - pool->tasks_available.store(Someone_Waiting); - if (!pool->running) { - // do not leave the word published on the way out: a worker still on its way to - // futex_wait would sleep on it, and the destroyer does not broadcast again once it - // has committed to its first join - pool->tasks_available.store(Nobody_Waiting); - futex_broadcast(&pool->tasks_available); - break; + { + Footex epoch = pool->tasks_available.load(); + pool->sleeping.fetch_add(1, std::memory_order_relaxed); + std::atomic_thread_fence(std::memory_order_seq_cst); + + // NOTE: a task added before `sleeping` was raised woke nobody, so look again + bool has_tasks = false; + for (Thread &t : pool->threads) { + has_tasks |= thread_pool_queue_has_tasks(&t); + } + if (!has_tasks && pool->running.load()) { + futex_wait(&pool->tasks_available, epoch); + } + pool->sleeping.fetch_sub(1, std::memory_order_relaxed); } - futex_wait(&pool->tasks_available, Someone_Waiting); main_loop_continue:; } From 5fbcbd17da14f962d018a2ab52b070f5eb980326 Mon Sep 17 00:00:00 2001 From: gingerBill Date: Fri, 2 Oct 2026 11:44:50 +0100 Subject: [PATCH 13/17] Compare entity source order by each file's index in its package rather than its name; search the initialization-order graph's edges in parallel --- src/checker.cpp | 86 ++++++++++++++++++++++++++++++++++--------------- src/parser.hpp | 1 + 2 files changed, 61 insertions(+), 26 deletions(-) diff --git a/src/checker.cpp b/src/checker.cpp index bd92cad9e..ae2729b9a 100644 --- a/src/checker.cpp +++ b/src/checker.cpp @@ -96,12 +96,17 @@ gb_internal int entity_source_order_cmp(Entity *x, Entity *y) { } } if (x->file != y->file) { - String fullpath_x = x->file ? x->file->fullpath : (String{}); - String fullpath_y = y->file ? y->file->fullpath : (String{}); - String file_x = filename_from_path(fullpath_x); - String file_y = filename_from_path(fullpath_y); + if (x->file != nullptr && y->file != nullptr && x->file->pkg != nullptr && x->file->pkg == y->file->pkg) { + // NOTE(bill): the same order as by name, as a package's files are sorted by it + cmp = i32_cmp(x->file->index_in_pkg, y->file->index_in_pkg); + } else { + String fullpath_x = x->file ? x->file->fullpath : (String{}); + String fullpath_y = y->file ? y->file->fullpath : (String{}); + String file_x = filename_from_path(fullpath_x); + String file_y = filename_from_path(fullpath_y); - cmp = string_compare(file_x, file_y); + cmp = string_compare(file_x, file_y); + } if (cmp) { return cmp; } @@ -3306,32 +3311,23 @@ gb_internal gb_inline bool is_entity_a_dependency(Entity *e) { return false; } -gb_internal Array generate_entity_dependency_graph(CheckerInfo *info, Arena *arena) { - PtrMap M_vars = {}; - map_init(&M_vars, info->entities.count); - defer (map_destroy(&M_vars)); +struct EntityGraphEdgesChunk { + PtrMap *vars; + Slice nodes; +}; - auto G = array_make(arena_allocator(arena), 0, info->entities.count); - for (Entity *e : info->entities) { - if (e == nullptr || e->kind != Entity_Variable || !is_entity_a_dependency(e)) { - continue; - } - EntityGraphNode *n = arena_alloc_item(arena); - n->entity = e; - map_set(&M_vars, e, n); - array_add(&G, n); - } +// NOTE(bill): A variable depends on every variable reachable from its declaration through procedures and constants, +// as its initializer may read any of them. Variables are not walked through, as they are ordered by their own edges. +// Each node's search only reads, and only writes its own successors, so the searches run in parallel. +gb_internal WORKER_TASK_PROC(generate_entity_dependency_graph_edges_worker) { + EntityGraphEdgesChunk *chunk = cast(EntityGraphEdgesChunk *)data; - TIME_SECTION("generate_entity_dependency_graph: Calculate edges"); - - // NOTE(bill): A variable depends on every variable reachable from its declaration through procedures and constants, - // as its initializer may read any of them. Variables are not walked through, as they are ordered by their own edges. PtrSet visited = {}; defer (ptr_set_destroy(&visited)); auto stack = array_make(heap_allocator(), 0, 64); defer (array_free(&stack)); - for (EntityGraphNode *n : G) { + for (EntityGraphNode *n : chunk->nodes) { ptr_set_clear(&visited); array_clear(&stack); @@ -3353,11 +3349,10 @@ gb_internal Array generate_entity_dependency_graph(CheckerInf continue; } if (dep->kind == Entity_Variable) { - EntityGraphNode **m = map_get(&M_vars, dep); + EntityGraphNode **m = map_get(chunk->vars, dep); // NOTE(bill): a variable naming itself, e.g. `t: struct { next: ^type_of(t) }`, is not an initialization cycle if (m != nullptr && *m != n) { entity_graph_node_set_add(&n->succ, *m); - entity_graph_node_set_add(&(*m)->pred, n); } continue; } @@ -3372,6 +3367,44 @@ gb_internal Array generate_entity_dependency_graph(CheckerInf } } } + return 0; +} + +gb_internal Array generate_entity_dependency_graph(CheckerInfo *info, Arena *arena) { + PtrMap M_vars = {}; + map_init(&M_vars, info->entities.count); + defer (map_destroy(&M_vars)); + + auto G = array_make(arena_allocator(arena), 0, info->entities.count); + for (Entity *e : info->entities) { + if (e == nullptr || e->kind != Entity_Variable || !is_entity_a_dependency(e)) { + continue; + } + EntityGraphNode *n = arena_alloc_item(arena); + n->entity = e; + map_set(&M_vars, e, n); + array_add(&G, n); + } + + TIME_SECTION("generate_entity_dependency_graph: Calculate edges"); + + isize const CHUNK_SIZE = 32; + auto chunks = array_make(heap_allocator(), 0, G.count/CHUNK_SIZE + 1); + defer (array_free(&chunks)); + for (isize i = 0; i < G.count; i += CHUNK_SIZE) { + array_add(&chunks, EntityGraphEdgesChunk{&M_vars, slice(slice_from_array(G), i, gb_min(i + CHUNK_SIZE, G.count))}); + } + for (EntityGraphEdgesChunk &chunk : chunks) { + thread_pool_add_task(generate_entity_dependency_graph_edges_worker, &chunk); + } + thread_pool_wait(); + + // NOTE: in the order of the nodes, so each node's predecessors are added in the order they were before + for (EntityGraphNode *n : G) { + FOR_PTR_SET(m, n->succ) { + entity_graph_node_set_add(&m->pred, n); + } + } TIME_SECTION("generate_entity_dependency_graph: Dependency Count Checker"); for_array(i, G) { @@ -5940,6 +5973,7 @@ gb_internal void check_create_file_scopes(Checker *c) { isize total_pkg_decl_count = 0; for_array(j, pkg->files) { AstFile *f = pkg->files[j]; + f->index_in_pkg = cast(i32)j; string_map_set(&c->info.files, f->fullpath, f); create_scope_from_file(nullptr, f); diff --git a/src/parser.hpp b/src/parser.hpp index f9d8da6af..d1988f89b 100644 --- a/src/parser.hpp +++ b/src/parser.hpp @@ -106,6 +106,7 @@ enum AstDelayQueueKind { struct AstFile { i32 id; + i32 index_in_pkg; // once the package's files are sorted by name, see `check_create_file_scopes` u32 flags; AstPackage * pkg; Scope * scope; From ff9d3584f4a471e09c61f8f4c7fe48d1b2a71c91 Mon Sep 17 00:00:00 2001 From: gingerBill Date: Fri, 2 Oct 2026 12:07:06 +0100 Subject: [PATCH 14/17] Per-thread arrays instead of MPSC queues for entities, definitions and untyped expressions; walk the minimum dependency set from per-task stacks, and scan its root definitions in parallel --- src/array.cpp | 13 +++ src/checker.cpp | 217 +++++++++++++++++++++++++------------------- src/checker.hpp | 7 +- src/thread_pool.cpp | 53 +++++++++++ 4 files changed, 192 insertions(+), 98 deletions(-) diff --git a/src/array.cpp b/src/array.cpp index 5176f2511..853a0fc03 100644 --- a/src/array.cpp +++ b/src/array.cpp @@ -94,6 +94,19 @@ gb_internal Slice slice_make(gbAllocator const &allocator, isize count) { return s; } + +template +gb_internal Slice slice_make_aligned(gbAllocator const &allocator, isize count, isize alignment) { + GB_ASSERT(count >= 0); + Slice s = {}; + s.data = cast(T *)gb_alloc_align(allocator, count*gb_size_of(T), alignment); + if (count > 0) { + GB_ASSERT(s.data != nullptr); + } + s.count = count; + return s; +} + template gb_internal void slice_init(Slice *s, gbAllocator const &allocator, isize count) { GB_ASSERT(count >= 0); diff --git a/src/checker.cpp b/src/checker.cpp index ae2729b9a..cc8a3d18d 100644 --- a/src/checker.cpp +++ b/src/checker.cpp @@ -1679,8 +1679,8 @@ gb_internal void init_checker_info(CheckerInfo *i) { array_init(&i->all_procedures, a); mpsc_init(&i->all_procedures_queue, a); - mpsc_init(&i->entity_queue, a); // 1<<20); - mpsc_init(&i->definition_queue, a); //); // 1<<20); + per_thread_array_init(&i->entity_queue, global_thread_pool.threads.count); + per_thread_array_init(&i->definition_queue, global_thread_pool.threads.count); mpsc_init(&i->required_global_variable_queue, a); // 1<<10); mpsc_init(&i->required_foreign_imports_through_force_queue, a); // 1<<10); mpsc_init(&i->foreign_imports_to_check_fullpaths, a); // 1<<10); @@ -1713,8 +1713,8 @@ gb_internal void destroy_checker_info(CheckerInfo *i) { mpsc_destroy(&i->all_procedures_queue); - mpsc_destroy(&i->entity_queue); - mpsc_destroy(&i->definition_queue); + per_thread_array_destroy(&i->entity_queue); + per_thread_array_destroy(&i->definition_queue); mpsc_destroy(&i->required_global_variable_queue); mpsc_destroy(&i->required_foreign_imports_through_force_queue); mpsc_destroy(&i->foreign_imports_to_check_fullpaths); @@ -1807,7 +1807,7 @@ gb_internal void init_checker(Checker *c) { array_init(&c->procs_to_check, heap_allocator(), 0, 1<<20); array_init(&c->nested_proc_lits, heap_allocator(), 0, 1<<20); - mpsc_init(&c->global_untyped_queue, a); // , 1<<20); + per_thread_array_init(&c->global_untyped_queue, global_thread_pool.threads.count); mpsc_init(&c->soa_types_to_complete, a); // , 1<<20); init_checker_context(&c->builtin_ctx, c); @@ -1820,7 +1820,7 @@ gb_internal void destroy_checker(Checker *c) { array_free(&c->nested_proc_lits); array_free(&c->procs_to_check); - mpsc_destroy(&c->global_untyped_queue); + per_thread_array_destroy(&c->global_untyped_queue); mpsc_destroy(&c->soa_types_to_complete); } @@ -2079,7 +2079,7 @@ gb_internal void add_entity_definition(CheckerInfo *i, Ast *identifier, Entity * GB_ASSERT(entity != nullptr); identifier->Ident.entity = entity; entity->identifier = identifier; - mpsc_enqueue(&i->definition_queue, entity); + per_thread_array_add(&i->definition_queue, entity); } gb_internal bool redeclaration_error(String name, Entity *prev, Entity *found) { @@ -2332,19 +2332,16 @@ gb_internal void add_entity_and_decl_info(CheckerContext *c, Ast *identifier, En e->pkg = c->pkg; d->entity.store(e); - isize queue_count = -1; - bool is_lazy = false; - - is_lazy = (e->flags & EntityFlag_Lazy) == EntityFlag_Lazy; + bool is_lazy = (e->flags & EntityFlag_Lazy) == EntityFlag_Lazy; if (!is_lazy) { - queue_count = mpsc_enqueue(&info->entity_queue, e); + per_thread_array_add(&info->entity_queue, e); } if (e->token.pos.file_id != 0) { e->order_in_src = cast(u64)(e->token.pos.file_id)<<32 | u32(e->token.pos.offset); } else { GB_ASSERT(!is_lazy); - e->order_in_src = cast(u64)(1+queue_count); + e->order_in_src = 1 + info->entities_without_file.fetch_add(1); } } @@ -2912,73 +2909,91 @@ gb_internal void add_dependency_to_set(Checker *c, Entity *entity) { } } -gb_internal WORKER_TASK_PROC(add_dependency_to_set_worker) { - Checker *c = global_checker_ptr.load(std::memory_order_relaxed); - Entity *entity = cast(Entity *)data; - if (entity == nullptr) { - return 0; - } - +gb_internal bool min_dep_visit(Entity *entity) { if (entity->type != nullptr && is_type_polymorphic(entity->type)) { DeclInfo *decl = decl_info_of_entity(entity); if (decl != nullptr && decl->gen_proc_type == nullptr) { - return 0; + return false; + } + } + return entity->min_dep_count.fetch_add(1, std::memory_order_relaxed) == 0; +} + +// NOTE(bill): a task walks from its own stack of entities, and hands half of it to a new task once it is large, +// as with a task for each dependency, adding the tasks was most of the work +enum { MIN_DEP_TASK_SPLIT = 256 }; + +gb_internal WORKER_TASK_PROC(add_dependency_to_set_worker) { + Checker *c = global_checker_ptr.load(std::memory_order_relaxed); + Array *stack = cast(Array *)data; + + while (stack->count > 0) { + Entity *entity = array_pop(stack); + if (entity == nullptr || !min_dep_visit(entity)) { + continue; + } + DeclInfo *decl = decl_info_of_entity(entity); + if (decl == nullptr) { + continue; + } + for (TypeInfoPair const tt : decl->type_info_deps) { + add_min_dep_type_info(c, tt.type); + } + + FOR_PTR_SET(e, decl->deps) { + Entity *fl = nullptr; + switch (e->kind) { + case Entity_Procedure: + if (e->Procedure.is_foreign) { + fl = e->Procedure.foreign_library; + } + break; + case Entity_Variable: + if (e->Variable.is_foreign) { + fl = e->Variable.foreign_library; + } + break; + } + if (fl != nullptr) { + GB_ASSERT_MSG(fl->kind == Entity_LibraryName && + (fl->flags&EntityFlag_Used), + "%.*s", LIT(entity->token.string)); + array_add(stack, fl); + } + } + + FOR_PTR_SET(e, decl->deps) { + if (e->min_dep_count.load(std::memory_order_relaxed) == 0) { + array_add(stack, e); + } + } + + if (stack->count >= MIN_DEP_TASK_SPLIT) { + isize half = stack->count/2; + Array *other = gb_alloc_item(heap_allocator(), Array); + array_init(other, heap_allocator(), 0, MIN_DEP_TASK_SPLIT); + array_add_elems(other, stack->data, half); + gb_memmove(stack->data, stack->data + half, (stack->count - half)*gb_size_of(Entity *)); + stack->count -= half; + thread_pool_add_task(add_dependency_to_set_worker, other); } } - if (entity->min_dep_count.fetch_add(1, std::memory_order_relaxed) > 0) { - return 0; - } - - DeclInfo *decl = decl_info_of_entity(entity); - if (decl == nullptr) { - return 0; - } - for (TypeInfoPair const tt : decl->type_info_deps) { - add_min_dep_type_info(c, tt.type); - } - - FOR_PTR_SET(e, decl->deps) { - switch (e->kind) { - case Entity_Procedure: - if (e->Procedure.is_foreign) { - Entity *fl = e->Procedure.foreign_library; - if (fl != nullptr) { - GB_ASSERT_MSG(fl->kind == Entity_LibraryName && - (fl->flags&EntityFlag_Used), - "%.*s", LIT(entity->token.string)); - add_dependency_to_set_threaded(c, fl); - } - } - break; - case Entity_Variable: - if (e->Variable.is_foreign) { - Entity *fl = e->Variable.foreign_library; - if (fl != nullptr) { - GB_ASSERT_MSG(fl->kind == Entity_LibraryName && - (fl->flags&EntityFlag_Used), - "%.*s", LIT(entity->token.string)); - add_dependency_to_set_threaded(c, fl); - } - } - break; - } - } - - FOR_PTR_SET(e, decl->deps) { - add_dependency_to_set_threaded(c, e); - } - + array_free(stack); + gb_free(heap_allocator(), stack); return 0; } gb_internal void add_dependency_to_set_threaded(Checker *c, Entity *entity) { - if (entity == nullptr) { + if (entity == nullptr || entity->min_dep_count.load(std::memory_order_relaxed) > 0) { return; } - thread_pool_add_task(add_dependency_to_set_worker, entity); + Array *stack = gb_alloc_item(heap_allocator(), Array); + array_init(stack, heap_allocator(), 0, 64); + array_add(stack, entity); + thread_pool_add_task(add_dependency_to_set_worker, stack); } @@ -3036,26 +3051,43 @@ gb_internal void collect_testing_procedures_of_package(Checker *c, AstPackage *p } } +gb_internal WORKER_TASK_PROC(add_definitions_to_set_worker) { + Checker *c = global_checker_ptr.load(std::memory_order_relaxed); + Scope *builtin_scope = builtin_pkg->scope; + for (Entity *e : *cast(Slice *)data) { + if (e->scope == builtin_scope) { + if (e->type == nullptr) { + add_dependency_to_set_threaded(c, e); + } + } else if (e->kind == Entity_Procedure) { + if (e->Procedure.is_export) { + add_dependency_to_set_threaded(c, e); + } + } else if (e->kind == Entity_Variable) { + if (e->Variable.is_export) { + add_dependency_to_set_threaded(c, e); + } + } + } + return 0; +} + gb_internal void generate_minimum_dependency_set_internal(Checker *c, Entity *start) { // auto const &add_to_set = add_dependency_to_set; auto const &add_to_set = add_dependency_to_set_threaded; - Scope *builtin_scope = builtin_pkg->scope; - for_array(i, c->info.definitions) { - Entity *e = c->info.definitions[i]; - if (e->scope == builtin_scope) { - if (e->type == nullptr) { - add_to_set(c, e); - } - } else if (e->kind == Entity_Procedure) { - if (e->Procedure.is_export) { - add_to_set(c, e); - } - } else if (e->kind == Entity_Variable) { - if (e->Variable.is_export) { - add_to_set(c, e); - } + { + // NOTE(bill): in parallel, as nearly all definitions are of locals, which add nothing + isize const CHUNK_SIZE = 4096; + auto chunks = array_make >(heap_allocator(), 0, c->info.definitions.count/CHUNK_SIZE + 1); + defer (array_free(&chunks)); + for (isize i = 0; i < c->info.definitions.count; i += CHUNK_SIZE) { + array_add(&chunks, slice(slice_from_array(c->info.definitions), i, gb_min(i + CHUNK_SIZE, c->info.definitions.count))); } + for (Slice &chunk : chunks) { + thread_pool_add_task(add_definitions_to_set_worker, &chunk); + } + thread_pool_wait(); } for (Entity *e; mpsc_dequeue(&c->info.required_foreign_imports_through_force_queue, &e); /**/) { @@ -6714,7 +6746,7 @@ gb_internal void add_untyped_expressions(CheckerInfo *cinfo, UntypedExprInfoMap Ast *expr = entry.key; ExprInfo *info = entry.value; if (expr != nullptr && info != nullptr) { - mpsc_enqueue(&cinfo->checker->global_untyped_queue, UntypedExprInfo{expr, info}); + per_thread_array_add(&cinfo->checker->global_untyped_queue, UntypedExprInfo{expr, info}); } } map_clear(untyped); @@ -7323,19 +7355,11 @@ gb_internal bool check_unique_package_names(Checker *c) { } gb_internal void check_add_entities_from_queues(Checker *c) { - isize cap = c->info.entities.count + c->info.entity_queue.count.load(std::memory_order_relaxed); - array_reserve(&c->info.entities, cap); - for (Entity *e; mpsc_dequeue(&c->info.entity_queue, &e); /**/) { - array_add(&c->info.entities, e); - } + per_thread_array_gather(&c->info.entity_queue, &c->info.entities); } gb_internal void check_add_definitions_from_queues(Checker *c) { - isize cap = c->info.definitions.count + c->info.definition_queue.count.load(std::memory_order_relaxed); - array_reserve(&c->info.definitions, cap); - for (Entity *e; mpsc_dequeue(&c->info.definition_queue, &e); /**/) { - array_add(&c->info.definitions, e); - } + per_thread_array_gather(&c->info.definition_queue, &c->info.definitions); } gb_internal void check_merge_queues_into_arrays(Checker *c) { @@ -7718,8 +7742,8 @@ gb_internal void check_parsed_files(Checker *c) { TIME_SECTION("sanity checks"); check_merge_queues_into_arrays(c); - GB_ASSERT(c->info.entity_queue.count.load(std::memory_order_relaxed) == 0); - GB_ASSERT(c->info.definition_queue.count.load(std::memory_order_relaxed) == 0); + GB_ASSERT(per_thread_array_count(&c->info.entity_queue) == 0); + GB_ASSERT(per_thread_array_count(&c->info.definition_queue) == 0); TIME_SECTION("check instrumentation calls"); { @@ -7733,7 +7757,10 @@ gb_internal void check_parsed_files(Checker *c) { TIME_SECTION("add untyped expression values"); - for (UntypedExprInfo u = {}; mpsc_dequeue(&c->global_untyped_queue, &u); /**/) { + auto untyped = array_make(heap_allocator()); + defer (array_free(&untyped)); + per_thread_array_gather(&c->global_untyped_queue, &untyped); + for (UntypedExprInfo const &u : untyped) { GB_ASSERT(u.expr != nullptr && u.info != nullptr); if (is_type_typed(u.info->type)) { compiler_error("%s (type %s) is typed!", expr_to_string(u.expr), type_to_string(u.info->type)); diff --git a/src/checker.hpp b/src/checker.hpp index 52d43ad81..2bcf4fcec 100644 --- a/src/checker.hpp +++ b/src/checker.hpp @@ -793,8 +793,9 @@ struct CheckerInfo { BlockingMutex entry_point_mutex; - MPSCQueue definition_queue; - MPSCQueue entity_queue; + PerThreadArray definition_queue; + PerThreadArray entity_queue; + std::atomic entities_without_file; // for their `order_in_src` MPSCQueue required_global_variable_queue; MPSCQueue required_foreign_imports_through_force_queue; MPSCQueue foreign_imports_to_check_fullpaths; @@ -905,7 +906,7 @@ struct Checker { Array nested_proc_lits; - MPSCQueue global_untyped_queue; + PerThreadArray global_untyped_queue; MPSCQueue soa_types_to_complete; }; diff --git a/src/thread_pool.cpp b/src/thread_pool.cpp index 0f1e59c93..3509c9fc4 100644 --- a/src/thread_pool.cpp +++ b/src/thread_pool.cpp @@ -327,3 +327,56 @@ gb_internal THREAD_PROC(thread_pool_thread_proc) { return 0; } + +template +struct alignas(2*GB_CACHE_LINE_SIZE) PerThreadArraySlot { + Array array; + u8 padding[2*GB_CACHE_LINE_SIZE - gb_size_of(Array)]; +}; + +template +struct PerThreadArray { + Slice > slots; +}; + +template +gb_internal void per_thread_array_init(PerThreadArray *a, isize thread_count) { + isize align = gb_align_of(PerThreadArraySlot); + a->slots = slice_make_aligned >(permanent_allocator(), thread_count, align); + GB_ASSERT((cast(uintptr)a->slots.data & (align - 1)) == 0); + for (PerThreadArraySlot &slot : a->slots) { + array_init(&slot.array, heap_allocator()); + } +} + +template +gb_internal void per_thread_array_destroy(PerThreadArray *a) { + for (PerThreadArraySlot &slot : a->slots) { + array_free(&slot.array); + } +} + +template +gb_internal void per_thread_array_add(PerThreadArray *a, T const &value) { + isize index = current_thread_index(); + GB_ASSERT(0 <= index && index < a->slots.count); + array_add(&a->slots[index].array, value); +} + +template +gb_internal isize per_thread_array_count(PerThreadArray *a) { + isize count = 0; + for (PerThreadArraySlot &slot : a->slots) { + count += slot.array.count; + } + return count; +} + +template +gb_internal void per_thread_array_gather(PerThreadArray *a, Array *dst) { + array_reserve(dst, dst->count + per_thread_array_count(a)); + for (PerThreadArraySlot &slot : a->slots) { + array_add_elems(dst, slot.array.data, slot.array.count); + array_clear(&slot.array); + } +} From 14950b8ae9fef5aa990e90969525c94401db3089 Mon Sep 17 00:00:00 2001 From: gingerBill Date: Fri, 2 Oct 2026 12:55:09 +0100 Subject: [PATCH 15/17] Park nested procedures on their parent until its body is checked instead of re-queuing; per-entry #load caches and parallel #load_directory reads; don't merge dependencies into package-level declarations; separate the thread pool's contended fields onto their own cache lines --- src/check_builtin.cpp | 243 ++++++++++++++++++++++++------------------ src/check_decl.cpp | 2 +- src/checker.cpp | 26 ++++- src/checker.hpp | 9 +- src/thread_pool.cpp | 9 +- src/threading.cpp | 10 +- 6 files changed, 181 insertions(+), 118 deletions(-) diff --git a/src/check_builtin.cpp b/src/check_builtin.cpp index 6777ca8b1..701de7a67 100644 --- a/src/check_builtin.cpp +++ b/src/check_builtin.cpp @@ -2029,7 +2029,61 @@ gb_internal bool check_builtin_simd_operation(CheckerContext *c, Operand *operan return false; } -gb_internal bool cache_load_file_directive(CheckerContext *c, Ast *call, String const &original_string, bool err_on_not_found, LoadFileCache **cache_, LoadFileTier tier, bool use_mutex=true) { +// NOTE(bill, 2026-10-02) The cache entry of `path` loaded to at least `tier`. +// The map is only locked to find or add the entry, and the file is loaded under the entry's own mutex, so different files load at once. +gb_internal LoadFileCache *load_file_cache_entry(CheckerInfo *info, String const &path, LoadFileTier tier) { + LoadFileCache *cache = nullptr; + mutex_lock(&info->load_file_mutex); + LoadFileCache **cache_ptr = string_map_get(&info->load_file_cache, path); + if (cache_ptr != nullptr) { + cache = *cache_ptr; + } else { + cache = permanent_alloc_item(); + cache->path = path; + string_map_init(&cache->hashes, 32); + string_map_set(&info->load_file_cache, path, cache); + } + mutex_unlock(&info->load_file_mutex); + + MUTEX_GUARD(&cache->mutex); + if (tier > cache->tier) { + cache->tier = tier; + + TEMPORARY_ALLOCATOR_GUARD(); + char *c_str = alloc_cstring(temporary_allocator(), path); + + gbFile f = {}; + cache->file_error = gb_file_open(&f, c_str); + defer (gb_file_close(&f)); + + if (cache->file_error == gbFileError_None) { + cache->exists = true; + + switch(tier) { + case LoadFileTier_Exists: + // Nothing to do. + break; + case LoadFileTier_Contents: { + isize file_size = cast(isize)gb_file_size(&f); + if (file_size > 0) { + u8 *ptr = permanent_alloc_array(file_size+1); + gb_file_read_at(&f, ptr, file_size, 0); + ptr[file_size] = '\0'; + cache->data.text = ptr; + cache->data.len = file_size; + } + break; + } + default: + GB_PANIC("Unhandled LoadFileTier"); + break; + }; + } + } + return cache; +} + +gb_internal bool cache_load_file_directive(CheckerContext *c, Ast *call, String const &original_string, bool err_on_not_found, LoadFileCache **cache_, LoadFileTier tier) { ast_node(ce, CallExpr, call); ast_node(bd, BasicDirective, ce->proc); String builtin_name = bd->name.string; @@ -2051,75 +2105,12 @@ gb_internal bool cache_load_file_directive(CheckerContext *c, Ast *call, String } } - if (use_mutex) mutex_lock(&c->info->load_file_mutex); - defer (if (use_mutex) mutex_unlock(&c->info->load_file_mutex)); + LoadFileCache *cache = load_file_cache_entry(c->info, path, tier); + if (cache_) *cache_ = cache; - gbFileError file_error = gbFileError_None; - String data = {}; - bool exists = false; - LoadFileTier cache_tier = LoadFileTier_Invalid; - - LoadFileCache **cache_ptr = string_map_get(&c->info->load_file_cache, path); - LoadFileCache *cache = cache_ptr ? *cache_ptr : nullptr; - if (cache) { - file_error = cache->file_error; - data = cache->data; - exists = cache->exists; - cache_tier = cache->tier; - } - defer ({ - if (cache == nullptr) { - LoadFileCache *new_cache = permanent_alloc_item(); - new_cache->path = path; - new_cache->data = data; - new_cache->file_error = file_error; - new_cache->exists = exists; - new_cache->tier = cache_tier; - string_map_init(&new_cache->hashes, 32); - string_map_set(&c->info->load_file_cache, path, new_cache); - if (cache_) *cache_ = new_cache; - } else { - cache->data = data; - cache->file_error = file_error; - cache->exists = exists; - cache->tier = cache_tier; - if (cache_) *cache_ = cache; - } - }); - - if (tier > cache_tier) { - cache_tier = tier; - - TEMPORARY_ALLOCATOR_GUARD(); - char *c_str = alloc_cstring(temporary_allocator(), path); - - gbFile f = {}; - file_error = gb_file_open(&f, c_str); - defer (gb_file_close(&f)); - - if (file_error == gbFileError_None) { - exists = true; - - switch(tier) { - case LoadFileTier_Exists: - // Nothing to do. - break; - case LoadFileTier_Contents: { - isize file_size = cast(isize)gb_file_size(&f); - if (file_size > 0) { - u8 *ptr = permanent_alloc_array(file_size+1); - gb_file_read_at(&f, ptr, file_size, 0); - ptr[file_size] = '\0'; - data.text = ptr; - data.len = file_size; - } - break; - } - default: - GB_PANIC("Unhandled LoadFileTier"); - }; - } - } + mutex_lock(&cache->mutex); + gbFileError file_error = cache->file_error; + mutex_unlock(&cache->mutex); switch (file_error) { default: @@ -2253,6 +2244,62 @@ gb_internal int file_cache_sort_cmp(void const *x, void const *y) { return string_compare(a->path, b->path); } +// NOTE(bill): a directory may hold thousands of files, which are read one at a time otherwise. +// This thread and a few helper tasks take them in turn and this thread only waits for the ones a running helper has taken, +// and the job is freed by whichever of them finishes with it last. +struct LoadDirectoryPrefetch { + CheckerInfo * info; + Array paths; + std::atomic next; + std::atomic done; + std::atomic refs; +}; + +gb_internal void load_directory_prefetch_run(LoadDirectoryPrefetch *job) { + for (isize i = job->next.fetch_add(1); i < job->paths.count; i = job->next.fetch_add(1)) { + load_file_cache_entry(job->info, job->paths[i], LoadFileTier_Contents); + job->done.fetch_add(1); + } + if (job->refs.fetch_sub(1) == 1) { + array_free(&job->paths); + gb_free(heap_allocator(), job); + } +} + +gb_internal WORKER_TASK_PROC(load_directory_prefetch_worker) { + load_directory_prefetch_run(cast(LoadDirectoryPrefetch *)data); + return 0; +} + +gb_internal void load_directory_prefetch(CheckerInfo *info, Array const &list) { + isize const FILES_PER_HELPER = 64; + + LoadDirectoryPrefetch *job = gb_alloc_item(heap_allocator(), LoadDirectoryPrefetch); + job->info = info; + array_init(&job->paths, heap_allocator(), 0, list.count); + for (FileInfo const &fi : list) { + if (!fi.is_dir) { + array_add(&job->paths, fi.fullpath); + } + } + isize count = job->paths.count; + isize helpers = gb_min(global_thread_pool.threads.count - 1, count/FILES_PER_HELPER); + job->refs.store(cast(i32)(helpers + 1)); + for (isize i = 0; i < helpers; i++) { + thread_pool_add_task(load_directory_prefetch_worker, job); + } + + job->refs.fetch_add(1); // NOTE(bill): kept until every file is done + load_directory_prefetch_run(job); + while (job->done.load() < count) { + yield_thread(); + } + if (job->refs.fetch_sub(1) == 1) { + array_free(&job->paths); + gb_free(heap_allocator(), job); + } +} + gb_internal LoadDirectiveResult check_load_directory_directive(CheckerContext *c, Operand *operand, Ast *call, Type *type_hint, bool err_on_not_found) { ast_node(ce, CallExpr, call); ast_node(bd, BasicDirective, ce->proc); @@ -2301,39 +2348,26 @@ gb_internal LoadDirectiveResult check_load_directory_directive(CheckerContext *c bool ok = determine_path_from_string(ignore_mutex, call, base_dir, original_string, &path); gb_unused(ok); } - MUTEX_GUARD(&c->info->load_directory_mutex); - - - gbFileError file_error = gbFileError_None; - - Array file_caches = {}; - + // NOTE(bill): the map is only locked to find or add the directory's entry which is loaded under its own mutex + LoadDirectoryCache *cache = nullptr; + mutex_lock(&c->info->load_directory_mutex); LoadDirectoryCache **cache_ptr = string_map_get(&c->info->load_directory_cache, path); - LoadDirectoryCache *cache = cache_ptr ? *cache_ptr : nullptr; - if (cache) { - file_error = cache->file_error; + if (cache_ptr != nullptr) { + cache = *cache_ptr; + } else { + cache = permanent_alloc_item(); + cache->path = path; + string_map_set(&c->info->load_directory_cache, path, cache); } - defer ({ - if (cache == nullptr) { - LoadDirectoryCache *new_cache = permanent_alloc_item(); - new_cache->path = path; - new_cache->files = file_caches; - new_cache->file_error = file_error; - string_map_set(&c->info->load_directory_cache, path, new_cache); - - map_set(&c->info->load_directory_map, call, new_cache); - } else { - cache->file_error = file_error; - - map_set(&c->info->load_directory_map, call, cache); - } - }); + map_set(&c->info->load_directory_map, call, cache); + mutex_unlock(&c->info->load_directory_mutex); + MUTEX_GUARD(&cache->mutex); LoadDirectiveResult result = LoadDirective_Success; - - if (cache == nullptr) { + if (!cache->loaded) { + cache->loaded = true; Array list = {}; ReadDirectoryError rd_err = read_directory(path, &list); defer (array_free(&list)); @@ -2364,27 +2398,26 @@ gb_internal LoadDirectiveResult check_load_directory_directive(CheckerContext *c return LoadDirective_Error; } + load_directory_prefetch(c->info, list); + isize files_to_reserve = list.count+1; // always reserve 1 - file_caches = array_make(heap_allocator(), 0, files_to_reserve); - - mutex_lock(&c->info->load_file_mutex); - defer (mutex_unlock(&c->info->load_file_mutex)); + cache->files = array_make(heap_allocator(), 0, files_to_reserve); for (FileInfo fi : list) { - LoadFileCache *cache = nullptr; + LoadFileCache *file_cache = nullptr; if (fi.is_dir) { continue; } - if (cache_load_file_directive(c, call, fi.fullpath, err_on_not_found, &cache, LoadFileTier_Contents, /*use_mutex*/false)) { - array_add(&file_caches, cache); + if (cache_load_file_directive(c, call, fi.fullpath, err_on_not_found, &file_cache, LoadFileTier_Contents)) { + array_add(&cache->files, file_cache); } else { result = LoadDirective_Error; } } - array_sort(file_caches, file_cache_sort_cmp); + array_sort(cache->files, file_cache_sort_cmp); } @@ -2574,7 +2607,7 @@ gb_internal bool check_builtin_procedure_directive(CheckerContext *c, Operand *o LoadFileCache *cache = nullptr; if (cache_load_file_directive(c, call, original_string, true, &cache, LoadFileTier_Contents)) { - MUTEX_GUARD(&c->info->load_file_mutex); + MUTEX_GUARD(&cache->mutex); // TODO(bill): make these procedures fast :P u64 hash_value = 0; u64 *hash_value_ptr = string_map_get(&cache->hashes, hash_kind); diff --git a/src/check_decl.cpp b/src/check_decl.cpp index 008959936..2c225c61f 100644 --- a/src/check_decl.cpp +++ b/src/check_decl.cpp @@ -2321,7 +2321,7 @@ gb_internal void wait_for_entity(Entity *e) { gb_internal void add_deps_from_child_to_parent(DeclInfo *decl) { if (decl && decl->parent) { Scope *ps = decl->parent->scope; - if (ps->flags & (ScopeFlag_File & ScopeFlag_Pkg & ScopeFlag_Global)) { + if (ps->flags & (ScopeFlag_Pkg | ScopeFlag_Global)) { return; } else { // NOTE(bill): Add the dependencies from the procedure literal (lambda) diff --git a/src/checker.cpp b/src/checker.cpp index cc8a3d18d..d9e5f208a 100644 --- a/src/checker.cpp +++ b/src/checker.cpp @@ -207,6 +207,7 @@ gb_internal void init_decl_info(DeclInfo *d, Scope *scope, DeclInfo *parent) { ptr_set_init(&d->deps, 0); type_set_init(&d->type_info_deps, 0); d->labels.allocator = heap_allocator(); + d->nested_to_check.allocator = heap_allocator(); d->variadic_reuses.allocator = heap_allocator(); d->variadic_reuse_max_bytes = 0; d->variadic_reuse_max_align = 1; @@ -6521,6 +6522,17 @@ gb_internal bool check_proc_info(Checker *c, ProcInfo *pi, UntypedExprInfoMap *u } } + // NOTE(bill): the nested procedures that waited for this body, see `check_proc_info_worker_proc` + mutex_lock(&pi->decl->next_mutex); + Array nested = pi->decl->nested_to_check; + pi->decl->nested_to_check = {}; + pi->decl->nested_to_check.allocator = heap_allocator(); + mutex_unlock(&pi->decl->next_mutex); + for (ProcInfo *nested_pi : nested) { + thread_pool_add_task(check_proc_info_worker_proc, nested_pi); + } + array_free(&nested); + add_untyped_expressions(&c->info, ctx.untyped); rw_mutex_shared_lock(&ctx.decl->deps_mutex); @@ -6682,8 +6694,18 @@ gb_internal WORKER_TASK_PROC(check_proc_info_worker_proc) { if (parent->kind == Entity_Procedure && (parent->flags & EntityFlag_ProcBodyChecked) == 0) { Type *pt = base_type(parent->type); if (!pt->Proc.is_polymorphic || pt->Proc.is_poly_specialized) { - thread_pool_add_task(check_proc_info_worker_proc, pi); - return 1; + // NOTE(bill): waits for the parent's body to be checked, which then adds it again, + // rather than adding itself again and again until then + DeclInfo *pd = pi->decl->parent; + mutex_lock(&pd->next_mutex); + bool waiting = (parent->flags & EntityFlag_ProcBodyChecked) == 0; + if (waiting) { + array_add(&pd->nested_to_check, pi); + } + mutex_unlock(&pd->next_mutex); + if (waiting) { + return 1; + } } } } diff --git a/src/checker.hpp b/src/checker.hpp index 2bcf4fcec..57a3ae11b 100644 --- a/src/checker.hpp +++ b/src/checker.hpp @@ -207,10 +207,12 @@ struct VariadicReuseData { struct DeclInfo { DeclInfo * parent; // NOTE(bill): only used for procedure literals at the moment - BlockingMutex next_mutex; + BlockingMutex next_mutex; // also used for `nested_to_check` DeclInfo * next_child; DeclInfo * next_sibling; + Array nested_to_check; // nested procedures to check once this body is checked + Scope * scope; std::atomic entity; @@ -693,6 +695,8 @@ enum LoadFileTier { struct LoadFileCache { LoadFileTier tier; bool exists; + + BlockingMutex mutex; // for everything below String path; gbFileError file_error; String data; @@ -706,6 +710,9 @@ struct LoadDirectoryFile { }; struct LoadDirectoryCache { + bool loaded; + + BlockingMutex mutex; // for everything below String path; gbFileError file_error; Array files; diff --git a/src/thread_pool.cpp b/src/thread_pool.cpp index 3509c9fc4..7ab66a479 100644 --- a/src/thread_pool.cpp +++ b/src/thread_pool.cpp @@ -34,9 +34,10 @@ struct ThreadPool { Slice threads; std::atomic running; - Futex tasks_available; // bumped to wake a sleeping worker - std::atomic sleeping; // workers asleep on `tasks_available`, or about to be - Futex tasks_left; + // NOTE: on separate cache lines, as every task changes `tasks_left` + alignas(2*GB_CACHE_LINE_SIZE) Futex tasks_available; // bumped to wake a sleeping worker + std::atomic sleeping; // workers asleep on `tasks_available`, or about to be + alignas(2*GB_CACHE_LINE_SIZE) Futex tasks_left; }; // NOTE(bill): how many times an idle worker looks for a task before sleeping, so one adding small tasks @@ -49,7 +50,7 @@ gb_internal isize current_thread_index(void) { gb_internal void thread_pool_init(ThreadPool *pool, isize worker_count, char const *worker_name) { pool->threads_allocator = permanent_allocator(); - slice_init(&pool->threads, pool->threads_allocator, worker_count + 1); + pool->threads = slice_make_aligned(pool->threads_allocator, worker_count + 1, gb_align_of(Thread)); // NOTE: this needs to be initialized before any thread starts pool->running.store(true, std::memory_order_seq_cst); diff --git a/src/threading.cpp b/src/threading.cpp index 28882ea24..fe890583b 100644 --- a/src/threading.cpp +++ b/src/threading.cpp @@ -51,11 +51,11 @@ typedef struct TaskRingBuffer { std::atomic buffer; } TaskRingBuffer; +// NOTE(bill, 2026-10-02): each on cache lines of its own as "thieves" take from `top` whilst the owner works at `bottom` typedef struct TaskQueue { - std::atomic top; - std::atomic bottom; - - std::atomic ring; + alignas(2*GB_CACHE_LINE_SIZE) std::atomic top; + alignas(2*GB_CACHE_LINE_SIZE) std::atomic bottom; + alignas(2*GB_CACHE_LINE_SIZE) std::atomic ring; } TaskQueue; struct Thread { @@ -74,7 +74,7 @@ struct Thread { struct Arena *permanent_arena; struct Arena *temporary_arena; - std::atomic *> waiting_futex; + alignas(2*GB_CACHE_LINE_SIZE) std::atomic *> waiting_futex; std::atomic waiting_value; std::atomic waiting_for; }; From 6bc4b69ab80f9451236362751a3213dcfb2e805a Mon Sep 17 00:00:00 2001 From: gingerBill Date: Fri, 2 Oct 2026 13:03:48 +0100 Subject: [PATCH 16/17] Fix #7316: a polymorphic procedure in a constant compound literal is its instance, not nil --- src/check_expr.cpp | 2 +- tests/issues/run.bat | 1 + tests/issues/run.sh | 1 + tests/issues/test_issue_7316.odin | 47 +++++++++++++++++++++++++++++++ 4 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 tests/issues/test_issue_7316.odin diff --git a/src/check_expr.cpp b/src/check_expr.cpp index 50b59f0c1..3556017ca 100644 --- a/src/check_expr.cpp +++ b/src/check_expr.cpp @@ -1006,7 +1006,7 @@ gb_internal i64 check_distance_between_types(CheckerContext *c, Operand *operand PolyProcData poly_proc_data = {}; if (check_polymorphic_procedure_assignment(c, operand, type, operand->expr, &poly_proc_data)) { Entity *e = poly_proc_data.gen_entity; - add_type_and_value(c, operand->expr, Addressing_Value, e->type, {}); + add_type_and_value(c, operand->expr, Addressing_Value, e->type, exact_value_procedure(operand->expr)); Ast *expr = unparen_expr(operand->expr); if (expr->kind == Ast_SelectorExpr) { add_entity_use(c, expr->SelectorExpr.selector, e); diff --git a/tests/issues/run.bat b/tests/issues/run.bat index 28fee09f4..f54c28eaf 100644 --- a/tests/issues/run.bat +++ b/tests/issues/run.bat @@ -78,6 +78,7 @@ clang -c ..\test_issue_sysv_abi.c -o test_issue_sysv_abi_c.o || exit /b ..\..\..\odin run ..\test_issue_7482.odin %COMMON% || exit /b ..\..\..\odin run ..\test_issue_7562.odin %COMMON% -no-crt -no-thread-local || exit /b ..\..\..\odin run ..\test_issue_7562.odin %COMMON% -no-crt -no-thread-local -o:speed || exit /b +..\..\..\odin test ..\test_issue_7316.odin %COMMON% || exit /b ..\..\..\odin test ..\test_issue_7566.odin %COMMON% || exit /b ..\..\..\odin test ..\test_issue_7587.odin %COMMON% || exit /b ..\..\..\odin run ..\test_issue_7596.odin %COMMON% || exit /b diff --git a/tests/issues/run.sh b/tests/issues/run.sh index 209d2923d..15d208d9a 100755 --- a/tests/issues/run.sh +++ b/tests/issues/run.sh @@ -101,6 +101,7 @@ $ODIN check ../test_issue_7012.odin -no-entry-point $COMMON_CHECK $ODIN build ../test_issue_7037.odin $COMMON -o:none $ODIN run ../test_issue_7482.odin $COMMON $ODIN run ../test_issue_7564.odin $COMMON +$ODIN test ../test_issue_7316.odin $COMMON $ODIN test ../test_issue_7566.odin $COMMON $ODIN test ../test_issue_7587.odin $COMMON $ODIN run ../test_issue_7596.odin $COMMON diff --git a/tests/issues/test_issue_7316.odin b/tests/issues/test_issue_7316.odin new file mode 100644 index 000000000..7f0edd503 --- /dev/null +++ b/tests/issues/test_issue_7316.odin @@ -0,0 +1,47 @@ +// Tests issue #7316 https://github.com/odin-lang/Odin/issues/7316 +// A polymorphic procedure in a constant compound literal is its instance, not nil +package test_issues + +import "core:testing" + +Foo :: struct($N: int) { + data: [N]u8, + number: int, + update: proc(f: ^Foo(N), delta: int), + get: proc(f: Foo(N)) -> int, +} + +foo_update :: proc(f: ^Foo($N), delta: int) { f.number += delta } +foo_get :: proc(f: Foo($N)) -> int { return f.number } + +foo_init :: proc(f: ^Foo($N)) { + f^ = { + number = 333, + update = foo_update, + get = foo_get, + } +} + +identity :: proc(x: $T) -> T { return x } + +global_foo := Foo(4){number = 1, update = foo_update, get = foo_get} +global_procs := [2]proc(int) -> int{identity, identity} + +@(test) +test_issue_7316 :: proc(t: ^testing.T) { + f: Foo(10) + foo_init(&f) + testing.expect(t, f.update != nil) + testing.expect(t, f.get != nil) + f.update(&f, 5) + testing.expect_value(t, f.get(f), 338) + + g := Foo(10){number = 1, update = foo_update} + testing.expect(t, g.update != nil) + + procs := [2]proc(int) -> int{identity, identity} + testing.expect_value(t, procs[1](7), 7) + + testing.expect(t, global_foo.update != nil) + testing.expect_value(t, global_procs[0](9), 9) +} From 0915bdcb9282e168e07c1cf5f7fa7188de83005e Mon Sep 17 00:00:00 2001 From: gingerBill Date: Fri, 2 Oct 2026 13:37:32 +0100 Subject: [PATCH 17/17] Reject procedure group candidates whose `$T/S` parameters cannot take the operands before checking their signatures; compare polymorphic instances by an identity hash before `are_types_identical` --- src/check_expr.cpp | 170 +++++++++++++++++++++++++++++++++++++++++++-- src/checker.hpp | 1 + 2 files changed, 167 insertions(+), 4 deletions(-) diff --git a/src/check_expr.cpp b/src/check_expr.cpp index 3556017ca..535325cb0 100644 --- a/src/check_expr.cpp +++ b/src/check_expr.cpp @@ -389,6 +389,149 @@ gb_internal void check_scope_decls(CheckerContext *c, Slice const &nodes, } } +gb_internal bool poly_specialization_shape_mismatch(Type *s, Type *o, isize depth) { + if (s == nullptr || o == nullptr || depth > 8) { + return false; + } + if (s->kind == Type_Generic) { + if (s->Generic.specialized == nullptr) { + return false; + } + s = s->Generic.specialized; + } + Type *bs = base_type(s); + Type *bo = base_type(o); + if (bs == nullptr || bo == nullptr || bs->kind == Type_Generic || bo->kind == Type_Generic) { + return false; + } + if (bs->kind != bo->kind) { + return true; + } + switch (bs->kind) { + case Type_Pointer: return poly_specialization_shape_mismatch(bs->Pointer.elem, bo->Pointer.elem, depth+1); + case Type_MultiPointer: return poly_specialization_shape_mismatch(bs->MultiPointer.elem, bo->MultiPointer.elem, depth+1); + case Type_Slice: return poly_specialization_shape_mismatch(bs->Slice.elem, bo->Slice.elem, depth+1); + case Type_DynamicArray: return poly_specialization_shape_mismatch(bs->DynamicArray.elem, bo->DynamicArray.elem, depth+1); + case Type_Array: return poly_specialization_shape_mismatch(bs->Array.elem, bo->Array.elem, depth+1); + case Type_FixedCapacityDynamicArray: return poly_specialization_shape_mismatch(bs->FixedCapacityDynamicArray.elem, bo->FixedCapacityDynamicArray.elem, depth+1); + case Type_Map: + return poly_specialization_shape_mismatch(bs->Map.key, bo->Map.key, depth+1) || + poly_specialization_shape_mismatch(bs->Map.value, bo->Map.value, depth+1); + case Type_Struct: + return bs->Struct.soa_kind != bo->Struct.soa_kind; + } + return false; +} + +gb_internal bool poly_candidate_cannot_match(TypeProc *pt, Array const &operands) { + if (pt->params == nullptr) { + return false; + } + auto const ¶ms = pt->params->Tuple.variables; + for (isize i = 0; i < params.count && i < operands.count; i++) { + if (pt->variadic && i == pt->variadic_index) { + continue; + } + Entity *param = params[i]; + Operand const &o = operands[i]; + if (param == nullptr || o.type == nullptr || o.deferred_untyped_arg || is_type_untyped(o.type)) { + continue; + } + + Type *t = param->type; + Type *ot = o.type; + if (param->kind == Entity_TypeName) { + if (o.mode != Addressing_Type) { + continue; + } + } else if (param->kind == Entity_Variable) { + if (o.mode != Addressing_Value && o.mode != Addressing_Variable && o.mode != Addressing_Constant) { + continue; + } + if (t != nullptr && t->kind == Type_Pointer) { + Type *bot = base_type(ot); + if (bot == nullptr || bot->kind != Type_Pointer) { + continue; + } + t = t->Pointer.elem; + ot = bot->Pointer.elem; + } + } else { + continue; + } + if (t != nullptr && t->kind == Type_Generic && t->Generic.specialized != nullptr && + poly_specialization_shape_mismatch(t, ot, 0)) { + return true; + } + } + return false; +} + +gb_internal u64 type_identity_hash(Type *t, isize depth) { + if (t == nullptr) { + return 0; + } + if (t->kind == Type_Named && t->Named.type_name != nullptr && t->Named.type_name->TypeName.is_type_alias) { + return type_identity_hash(t->Named.base, depth); + } + u64 h = (cast(u64)t->kind + 1) * 0x9e3779b97f4a7c15ull; + auto mix = [&](u64 x) { + h ^= x + 0x9e3779b97f4a7c15ull + (h << 6) + (h >> 2); + }; + if (depth > 8) { + return h; + } + switch (t->kind) { + case Type_Named: mix(cast(u64)cast(uintptr)t->Named.type_name); break; + case Type_Basic: mix(cast(u64)t->Basic.kind); break; + + case Type_Pointer: mix(type_identity_hash(t->Pointer.elem, depth+1)); break; + case Type_MultiPointer: mix(type_identity_hash(t->MultiPointer.elem, depth+1)); break; + case Type_SoaPointer: mix(type_identity_hash(t->SoaPointer.elem, depth+1)); break; + case Type_Slice: mix(type_identity_hash(t->Slice.elem, depth+1)); break; + case Type_DynamicArray: mix(type_identity_hash(t->DynamicArray.elem, depth+1)); break; + case Type_FixedCapacityDynamicArray: mix(type_identity_hash(t->FixedCapacityDynamicArray.elem, depth+1)); break; + case Type_EnumeratedArray: mix(type_identity_hash(t->EnumeratedArray.elem, depth+1)); break; + case Type_Matrix: mix(type_identity_hash(t->Matrix.elem, depth+1)); break; + + case Type_Array: + mix(cast(u64)t->Array.count); + mix(type_identity_hash(t->Array.elem, depth+1)); + break; + + case Type_Map: + mix(type_identity_hash(t->Map.key, depth+1)); + mix(type_identity_hash(t->Map.value, depth+1)); + break; + } + return h; +} + +gb_internal u64 tuple_identity_hash(Type *t) { + u64 h = 0x84222325cbf29ce4ull; + if (t != nullptr && t->kind == Type_Tuple) { + h ^= cast(u64)t->Tuple.variables.count * 31 + cast(u64)t->Tuple.is_packed; + for (Entity *e : t->Tuple.variables) { + h = (h ^ (cast(u64)e->kind * 0x100000001b3ull)) * 0x100000001b3ull; + h ^= type_identity_hash(e->type, 0) + 0x9e3779b97f4a7c15ull + (h << 6) + (h >> 2); + } + } + return h; +} + +gb_internal u64 proc_type_identity_hash(Type *t) { + t = base_type(t); + GB_ASSERT(t->kind == Type_Proc); + u64 h = cast(u64)t->Proc.calling_convention; + h = h*31 + cast(u64)t->Proc.c_vararg; + h = h*31 + cast(u64)t->Proc.variadic; + h = h*31 + cast(u64)t->Proc.diverging; + h = h*31 + cast(u64)t->Proc.optional_ok; + h ^= tuple_identity_hash(t->Proc.params) + 0x9e3779b97f4a7c15ull + (h << 6) + (h >> 2); + h ^= tuple_identity_hash(t->Proc.results) + 0x9e3779b97f4a7c15ull + (h << 6) + (h >> 2); + return h; +} + // Reuse an existing generated specialization `other`, scheduling its body if unchecked. // Caller must have released gen_procs->mutex first. gb_internal bool reuse_gen_polymorphic_procedure(Checker *checker, Entity *other, Ast *poly_def_node, PolyProcData *poly_proc_data) { @@ -475,6 +618,11 @@ gb_internal bool find_or_generate_polymorphic_procedure(CheckerContext *old_c, E } + // NOTE(bill): only whilst trying a procedure group's candidates as their errors are not reported + if (param_operands != nullptr && old_c->no_polymorphic_errors && poly_candidate_cannot_match(&src->Proc, *param_operands)) { + return false; + } + DeclInfo *old_decl = decl_info_of_entity(base_entity); if (old_decl == nullptr) { return false; @@ -534,7 +682,12 @@ gb_internal bool find_or_generate_polymorphic_procedure(CheckerContext *old_c, E mutex_unlock(&base_entity->Procedure.gen_procs_mutex); // @entity-mutex - for (Entity *other : gen_procs->procs) { + u64 hash = proc_type_identity_hash(final_proc_type); + for_array(i, gen_procs->procs) { + if (gen_procs->hashes[i] != hash) { + continue; + } + Entity *other = gen_procs->procs[i]; Type *pt = base_type(other->type); if (are_types_identical(pt, final_proc_type)) { rw_mutex_shared_unlock(&gen_procs->mutex); // @local-mutex @@ -550,6 +703,7 @@ gb_internal bool find_or_generate_polymorphic_procedure(CheckerContext *old_c, E } else { gen_procs = permanent_alloc_item(); gen_procs->procs.allocator = heap_allocator(); + gen_procs->hashes.allocator = heap_allocator(); base_entity->Procedure.gen_procs = gen_procs; mutex_unlock(&base_entity->Procedure.gen_procs_mutex); // @entity-mutex } @@ -573,8 +727,13 @@ gb_internal bool find_or_generate_polymorphic_procedure(CheckerContext *old_c, E return false; } + u64 hash = proc_type_identity_hash(final_proc_type); rw_mutex_shared_lock(&gen_procs->mutex); // @local-mutex - for (Entity *other : gen_procs->procs) { + for_array(i, gen_procs->procs) { + if (gen_procs->hashes[i] != hash) { + continue; + } + Entity *other = gen_procs->procs[i]; Type *pt = base_type(other->type); if (are_types_identical(pt, final_proc_type)) { rw_mutex_shared_unlock(&gen_procs->mutex); // @local-mutex @@ -587,9 +746,11 @@ gb_internal bool find_or_generate_polymorphic_procedure(CheckerContext *old_c, E // Re-check under the exclusive lock (the lookups above ran under a released shared lock) and // hold it across construction + array_add so find-then-publish is atomic. (@local-mutex) + u64 final_hash = proc_type_identity_hash(final_proc_type); rw_mutex_lock(&gen_procs->mutex); // @local-mutex - for (Entity *other : gen_procs->procs) { - if (are_types_identical(base_type(other->type), final_proc_type)) { + for_array(i, gen_procs->procs) { + Entity *other = gen_procs->procs[i]; + if (gen_procs->hashes[i] == final_hash && are_types_identical(base_type(other->type), final_proc_type)) { rw_mutex_unlock(&gen_procs->mutex); // @local-mutex return reuse_gen_polymorphic_procedure(nctx.checker, other, poly_def_node, poly_proc_data); } @@ -659,6 +820,7 @@ gb_internal bool find_or_generate_polymorphic_procedure(CheckerContext *old_c, E AstFile *file = base_entity->file; array_add(&gen_procs->procs, entity); + array_add(&gen_procs->hashes, proc_type_identity_hash(final_proc_type)); rw_mutex_unlock(&gen_procs->mutex); // @local-mutex ProcInfo *proc_info = permanent_alloc_item(); diff --git a/src/checker.hpp b/src/checker.hpp index 57a3ae11b..300395fb2 100644 --- a/src/checker.hpp +++ b/src/checker.hpp @@ -721,6 +721,7 @@ struct LoadDirectoryCache { struct GenProcsData { Array procs; + Array hashes; // `proc_type_identity_hash` of each of `procs` RwMutex mutex; };