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); +}