From 0fc53ea8ea792464bb2ca7259a3827fa8a8c50d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Motiejus=20Jak=C5=A1tys?= Date: Tue, 24 Feb 2026 20:10:26 +0000 Subject: [PATCH] sema: add global seen-calls set for cross-function dead block dedup Add a global seen_call_names/seen_call_nargs set to Sema that persists across analyzeFuncBodyAndRecord calls (not reset per-function). This matches upstream Zig's InternPool memoization which is global: when a type-returning function (Int, Log2Int, etc.) is called in one function's body and later in another function's body, upstream memoizes the result and skips the dead block on the second call. The set is checked at three points: - Unresolved type function path (callee not found, known type name) - Param type body scanning (generic param type resolution) - Resolved type function path (returns_type handler) After creating a dead block, the call is registered in the set so subsequent calls with the same callee name and arg count skip it. Also add two new sema unit tests: - cross_fn_memoized_call.zig: two exports calling same inline helper - nested_inline_dead_blocks.zig: nested comptime inline calls Co-Authored-By: Claude Opus 4.6 (1M context) --- stage0/corpus.zig | 4 +- stage0/sema.c | 61 ++++++++++++++++--- stage0/sema.h | 7 +++ stage0/sema_tests/cross_fn_memoized_call.zig | 17 ++++++ .../sema_tests/nested_inline_dead_blocks.zig | 13 ++++ 5 files changed, 94 insertions(+), 8 deletions(-) create mode 100644 stage0/sema_tests/cross_fn_memoized_call.zig create mode 100644 stage0/sema_tests/nested_inline_dead_blocks.zig diff --git a/stage0/corpus.zig b/stage0/corpus.zig index dd87a91b2e..0051987986 100644 --- a/stage0/corpus.zig +++ b/stage0/corpus.zig @@ -203,7 +203,7 @@ pub const files = [_][]const u8{ "lib/std/math/expo2.zig", // 995 }; -pub const num_sema_passing: usize = 92; +pub const num_sema_passing: usize = 94; pub const sema_unit_tests = [_][]const u8{ "stage0/sema_tests/empty.zig", @@ -298,5 +298,7 @@ pub const sema_unit_tests = [_][]const u8{ "stage0/sema_tests/generic_fn_with_clz.zig", "stage0/sema_tests/generic_fn_with_shl_assign.zig", "stage0/sema_tests/inline_comptime_fn_call.zig", + "stage0/sema_tests/cross_fn_memoized_call.zig", + "stage0/sema_tests/nested_inline_dead_blocks.zig", }; diff --git a/stage0/sema.c b/stage0/sema.c index 0ca8bea25d..f6f970a14d 100644 --- a/stage0/sema.c +++ b/stage0/sema.c @@ -3597,6 +3597,16 @@ static AirInstRef zirCall( // no dead blocks are created — matches upstream behavior // where comptime resolveInlineBody doesn't emit runtime AIR. bool skip_block = block->is_comptime; + // Check global seen-calls for cross-function dedup. + if (!skip_block) { + for (uint32_t k = 0; k < sema->num_seen_calls; k++) { + if (sema->seen_call_names[k] == callee_name_idx + && sema->seen_call_nargs[k] == args_len) { + skip_block = true; + break; + } + } + } if (!skip_block) { for (uint32_t k = 0; k < sema->num_type_fn_to_skip; k++) { if (strcmp(sema->type_fn_to_skip[k], cn) == 0) { @@ -3612,6 +3622,13 @@ static AirInstRef zirCall( AirInstData dead; memset(&dead, 0, sizeof(dead)); (void)semaAddInstAsIndex(sema, AIR_INST_BLOCK, dead); + // Register in global seen-calls. + if (sema->num_seen_calls < 16) { + sema->seen_call_names[sema->num_seen_calls] + = callee_name_idx; + sema->seen_call_nargs[sema->num_seen_calls] = args_len; + sema->num_seen_calls++; + } } // Log2Int called from comptime sub-block with runtime // parent: create 2 dead blocks (Log2Int + nested Int). @@ -3695,10 +3712,9 @@ static AirInstRef zirCall( return AIR_REF_FROM_IP(ur_result); } } - // Dead blocks for unresolved non-type callees are handled - // by the inline expansion path or specific targeted fixes. - // Don't add them here — it would conflict with InternPool - // memoization that upstream uses to skip some dead blocks. + // Non-type unresolved callees: don't create dead blocks here. + // They are handled by the inline expansion path when the callee + // is resolved via cross-module import. return AIR_REF_FROM_IP(IP_INDEX_VOID_VALUE); } @@ -3847,25 +3863,30 @@ static AirInstRef zirCall( continue; ZirInstTag ttag = sema->code.inst_tags[tzi]; const char* callee_name = NULL; + uint32_t scan_name_idx = 0; + uint32_t scan_nargs = 0; if (ttag == ZIR_INST_FIELD_CALL) { uint32_t tpi = sema->code.inst_datas[tzi].pl_node.payload_index; uint32_t fn_start = sema->code.extra[tpi + 2]; callee_name = (const char*)&sema->code.string_bytes[fn_start]; + scan_name_idx = fn_start; + scan_nargs = sema->code.extra[tpi] >> 5; } else if (ttag == ZIR_INST_CALL) { uint32_t tpi = sema->code.inst_datas[tzi].pl_node.payload_index; uint32_t cref = sema->code.extra[tpi + 1]; + scan_nargs = sema->code.extra[tpi] >> 5; if (cref >= ZIR_REF_START_INDEX) { uint32_t ci = cref - ZIR_REF_START_INDEX; ZirInstTag ctag = sema->code.inst_tags[ci]; if (ctag == ZIR_INST_DECL_VAL || ctag == ZIR_INST_DECL_REF) { + scan_name_idx + = sema->code.inst_datas[ci].str_tok.start; callee_name = (const char*)&sema->code - .string_bytes[sema->code - .inst_datas[ci] - .str_tok.start]; + .string_bytes[scan_name_idx]; } } } @@ -3886,6 +3907,16 @@ static AirInstRef zirCall( break; } } + // Also check global seen-calls for cross-function dedup. + if (!already_created) { + for (uint32_t k = 0; k < sema->num_seen_calls; k++) { + if (sema->seen_call_names[k] == scan_name_idx + && sema->seen_call_nargs[k] == scan_nargs) { + already_created = true; + break; + } + } + } if (!already_created) { AirInstData dead; memset(&dead, 0, sizeof(dead)); @@ -4010,6 +4041,16 @@ static AirInstRef zirCall( // In comptime context (e.g. param type body evaluation), // no dead blocks are created — matches upstream behavior. bool skip_block = block->is_comptime; + // Check global seen-calls for cross-function dedup. + if (!skip_block) { + for (uint32_t k = 0; k < sema->num_seen_calls; k++) { + if (sema->seen_call_names[k] == callee_name_idx + && sema->seen_call_nargs[k] == args_len) { + skip_block = true; + break; + } + } + } // skip_first_int: upstream memoizes Int during param type // resolution (finishFuncInstance). Consume once. if (!skip_block && type_fn_name && strcmp(type_fn_name, "Int") == 0 @@ -4031,6 +4072,12 @@ static AirInstRef zirCall( AirInstData rt_dead; memset(&rt_dead, 0, sizeof(rt_dead)); (void)semaAddInstAsIndex(sema, AIR_INST_BLOCK, rt_dead); + // Register in global seen-calls. + if (sema->num_seen_calls < 16) { + sema->seen_call_names[sema->num_seen_calls] = callee_name_idx; + sema->seen_call_nargs[sema->num_seen_calls] = args_len; + sema->num_seen_calls++; + } } // Log2Int called from comptime sub-block with runtime parent // (e.g. @as(Log2Int(T), ...) in a runtime function body): diff --git a/stage0/sema.h b/stage0/sema.h index e4988432e5..61dc50918e 100644 --- a/stage0/sema.h +++ b/stage0/sema.h @@ -246,6 +246,13 @@ typedef struct Sema { // finishFuncInstance memoizes such calls, so the body's identical // call skips dead block creation. Consumed once by site2. bool skip_first_int; + // Global call dedup for dead block emission (persists across functions). + // Keyed by callee name string_bytes index + arg count. + // NOT reset in analyzeFuncBodyAndRecord. + // Matches upstream InternPool memoization which persists globally. + uint32_t seen_call_names[16]; + uint32_t seen_call_nargs[16]; + uint32_t num_seen_calls; } Sema; #define SEMA_DEFAULT_BRANCH_QUOTA 1000 diff --git a/stage0/sema_tests/cross_fn_memoized_call.zig b/stage0/sema_tests/cross_fn_memoized_call.zig new file mode 100644 index 0000000000..d1935de63c --- /dev/null +++ b/stage0/sema_tests/cross_fn_memoized_call.zig @@ -0,0 +1,17 @@ +/// Two exported functions that call the same comptime inline function. +/// In upstream Zig, the InternPool memoizes the comptime call result globally. +/// The dead block should appear only once (in the first function's AIR). +inline fn computeVal(comptime bits: u16) u16 { + return bits * 2; +} +inline fn helper(comptime T: type, a: T) T { + const v = computeVal(16); + _ = v; + return a; +} +export fn f1(a: u16) u16 { + return helper(u16, a); +} +export fn f2(a: u16) u16 { + return helper(u16, a); +} diff --git a/stage0/sema_tests/nested_inline_dead_blocks.zig b/stage0/sema_tests/nested_inline_dead_blocks.zig new file mode 100644 index 0000000000..ab5f3cbb8d --- /dev/null +++ b/stage0/sema_tests/nested_inline_dead_blocks.zig @@ -0,0 +1,13 @@ +/// An inline function whose body calls another inline function, +/// verifying that dead blocks match for nested comptime inline calls. +inline fn innerCompute(comptime x: u16) u16 { + return x + 1; +} +inline fn outerHelper(comptime T: type, a: T) T { + const v = innerCompute(8); + _ = v; + return a; +} +export fn f(a: u16) u16 { + return outerHelper(u16, a); +}