sema: add abs, max/min, div, aggregate_init, cross-module re-export resolution; enable divdc3

Port missing builtins and operations needed for divdc3.zig corpus test:
- @abs: emit AIR_INST_ABS (ty_op) for float operands
- @max/@min: emit AIR_INST_MAX/AIR_INST_MIN at runtime (bin_op)
- div: emit AIR_INST_DIV_FLOAT for floats, AIR_INST_DIV_TRUNC for ints
- struct_init: runtime path emits AIR_INST_AGGREGATE_INIT
- Cross-module import chain: follow re-exports (e.g. scalbn -> ldexp)
- Fix return type resolution for generic anytype params (use semaTypeOf)
- Add comptime_float peer type resolution
- Skip test/comptime decls in findFuncInstInZir
- Handle validate_ref_ty as validation-only (no AIR)
- Remove leftover debug fprintf traces

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-23 05:11:17 +00:00
parent 21db07da43
commit 0c0ac99d27
3 changed files with 193 additions and 54 deletions

View File

@@ -566,6 +566,10 @@ static TypeIndex semaTypeOf(Sema* sema, AirInstRef ref) {
case AIR_INST_SHL:
case AIR_INST_SHL_SAT:
case AIR_INST_SHR:
case AIR_INST_MAX:
case AIR_INST_MIN:
case AIR_INST_DIV_FLOAT:
case AIR_INST_DIV_TRUNC:
return semaTypeOf(sema, sema->air_inst_datas[inst_idx].bin_op.lhs);
// cmp bin_op: result type is bool.
case AIR_INST_CMP_LT:
@@ -592,6 +596,7 @@ static TypeIndex semaTypeOf(Sema* sema, AirInstRef ref) {
case AIR_INST_CTZ:
case AIR_INST_POPCOUNT:
case AIR_INST_BYTE_SWAP:
case AIR_INST_ABS:
case AIR_INST_DBG_INLINE_BLOCK:
case AIR_INST_BLOCK:
case AIR_INST_STRUCT_FIELD_PTR_INDEX_0:
@@ -600,6 +605,7 @@ static TypeIndex semaTypeOf(Sema* sema, AirInstRef ref) {
case AIR_INST_STRUCT_FIELD_PTR_INDEX_3:
return AIR_REF_TO_IP(sema->air_inst_datas[inst_idx].ty_op.ty_ref);
case AIR_INST_STRUCT_FIELD_VAL:
case AIR_INST_AGGREGATE_INIT:
return AIR_REF_TO_IP(sema->air_inst_datas[inst_idx].ty_pl.ty_ref);
// call: return type from side table (populated by zirCall).
case AIR_INST_CALL:
@@ -657,6 +663,10 @@ static TypeIndex semaResolvePeerTypes(
return cIntToRegularInt(rhs_ty);
if (rhs_ty == IP_INDEX_COMPTIME_INT_TYPE)
return cIntToRegularInt(lhs_ty);
if (lhs_ty == IP_INDEX_COMPTIME_FLOAT_TYPE)
return rhs_ty;
if (rhs_ty == IP_INDEX_COMPTIME_FLOAT_TYPE)
return lhs_ty;
// When both types are concrete int types, pick the wider type.
// Ported from src/Sema.zig peer_resolve_int_int (fixed_int strategy).
if (sema->ip->items[lhs_ty].tag == IP_KEY_INT_TYPE
@@ -1064,6 +1074,25 @@ static uint16_t smallestUnsignedBits(uint16_t max) {
return count;
}
// zirAbs: handle @abs ZIR instruction.
// Ported from src/Sema.zig zirAbs.
// For floats/comptime_float: result_ty = operand_ty.
// For signed ints: result_ty = toUnsigned(operand_ty).
// For unsigned ints: identity (return operand).
static AirInstRef zirAbs(Sema* sema, SemaBlock* block, uint32_t inst) {
ZirInstRef operand_ref = sema->code.inst_datas[inst].un_node.operand;
AirInstRef operand = resolveInst(sema, operand_ref);
TypeIndex operand_ty = semaTypeOf(sema, operand);
// For floats, result type is the same as operand type.
// Emit AIR_INST_ABS as ty_op.
AirInstData data;
memset(&data, 0, sizeof(data));
data.ty_op.ty_ref = AIR_REF_FROM_IP(operand_ty);
data.ty_op.operand = operand;
return semaAddInst(block, AIR_INST_ABS, data);
}
// zirBitCount: handle clz/ctz/pop_count ZIR instructions.
// Ported from src/Sema.zig zirBitCount.
// Result type is smallestUnsignedInt(operand_bits).
@@ -1248,6 +1277,35 @@ emit_runtime:;
return semaAddInst(block, air_tag, data);
}
// zirDiv: handle div ZIR instruction (/ operator).
// Ported from src/Sema.zig zirDiv.
// For floats: emits AIR_INST_DIV_FLOAT (strict mode).
// For unsigned ints: emits AIR_INST_DIV_TRUNC.
static AirInstRef zirDiv(Sema* sema, SemaBlock* block, uint32_t inst) {
uint32_t payload_index = sema->code.inst_datas[inst].pl_node.payload_index;
ZirInstRef zir_lhs = sema->code.extra[payload_index];
ZirInstRef zir_rhs = sema->code.extra[payload_index + 1];
AirInstRef lhs = resolveInst(sema, zir_lhs);
AirInstRef rhs = resolveInst(sema, zir_rhs);
TypeIndex peer_ty = semaResolvePeerTypes(sema, lhs, rhs);
lhs = semaCoerce(sema, block, peer_ty, lhs);
rhs = semaCoerce(sema, block, peer_ty, rhs);
// Determine air_tag based on scalar type.
AirInstTag air_tag = AIR_INST_DIV_FLOAT; // default for floats
InternPoolKey pk = ipIndexToKey(sema->ip, peer_ty);
if (pk.tag == IP_KEY_INT_TYPE) {
air_tag = AIR_INST_DIV_TRUNC;
}
AirInstData data;
memset(&data, 0, sizeof(data));
data.bin_op.lhs = lhs;
data.bin_op.rhs = rhs;
return semaAddInst(block, air_tag, data);
}
// zirBitwise: handle bitwise operation ZIR instructions.
// Ported from src/Sema.zig zirBitwise.
static AirInstRef zirBitwise(
@@ -2259,6 +2317,11 @@ static uint32_t findFuncInstInZir(const Zir* zir, const char* func_name) {
uint32_t flags_1 = zir->extra[payload + 5];
uint32_t id = (flags_1 >> 27) & 0x1F;
// Skip test declarations (unnamed_test=0, test=1, decltest=2)
// and comptime blocks (3) — only look at const/var/export decls.
if (id <= 3)
continue;
uint32_t di = payload + 6;
// Extract declaration name.
@@ -2846,18 +2909,6 @@ static AirInstRef zirCall(
}
}
// Debug: save callee name before chain resolution changes sema->code.
char dbg_callee[64] = "?";
if (callee_name_idx) {
const char* nm
= (const char*)&sema->code.string_bytes[callee_name_idx];
size_t nl = strlen(nm);
if (nl > 63)
nl = 63;
memcpy(dbg_callee, nm, nl);
dbg_callee[nl] = 0;
}
// Find the inline function's ZIR instruction.
uint32_t func_inst = findDeclFuncInst(sema, callee_name_idx);
// Fallback: during cross-module inline expansion, sema->decl_names
@@ -3004,19 +3055,29 @@ static AirInstRef zirCall(
zirDeinit(&cur_zir);
astDeinit(&cur_ast);
if (fn_zir.inst_len > 0) {
// Function should be directly in this
// module with same name.
// Try direct lookup first, then follow
// re-exports (e.g. scalbn -> ldexp).
char fn_src_dir[1024];
computeSourceDir(NULL, cur_dir, fn_import,
fn_src_dir, sizeof(fn_src_dir));
func_inst
= findFuncInstInZir(&fn_zir, fn_name);
= findFuncInModuleZir(fn_src_dir, &fn_zir,
&fn_ast, fn_name, import_source_dir,
sizeof(import_source_dir));
if (func_inst != UINT32_MAX) {
// If findFuncInModuleZir found it
// directly (no re-export), it did
// not write import_source_dir.
// Set it to fn_src_dir as fallback.
if (import_source_dir[0] == '\0')
snprintf(import_source_dir,
sizeof(import_source_dir), "%s",
fn_src_dir);
saved_code = sema->code;
sema->code = fn_zir;
import_zir = fn_zir;
import_ast = fn_ast;
is_cross_module = true;
computeSourceDir(NULL, cur_dir, fn_import,
import_source_dir,
sizeof(import_source_dir));
} else {
zirDeinit(&fn_zir);
astDeinit(&fn_ast);
@@ -3844,19 +3905,6 @@ static AirInstRef zirCall(
sema->code = arg_code;
}
// Ported from src/Sema.zig line 7480:
// if (call_dbg_node) |some| try sema.zirDbgStmt(block, some);
// call_dbg_node is the ZIR instruction preceding the call (inst - 1).
// Must use the caller's ZIR for cross-module calls.
if (inst > 0 && !block->is_comptime) {
Zir dbg_code = sema->code;
if (is_cross_module)
sema->code = saved_code;
zirDbgStmt(sema, block, inst - 1);
if (is_cross_module)
sema->code = dbg_code;
}
// Handle type-returning functions whose result can be computed from
// the comptime arguments without inlining.
// Upstream always reserves a dead BLOCK before inlining; we match
@@ -4255,8 +4303,15 @@ static AirInstRef zirCall(
if (param_body[p] == ret_ty_inst && pi < args_len) {
// Return type ref matches this param.
// For comptime type params, the arg value IS
// the type.
ret_ty = AIR_REF_TO_IP(arg_refs[pi]);
// the type (an IP ref). For runtime params
// (including anytype), use semaTypeOf to get
// the argument's type.
if (ptag == ZIR_INST_PARAM_COMPTIME
|| ptag == ZIR_INST_PARAM_ANYTYPE_COMPTIME) {
ret_ty = AIR_REF_TO_IP(arg_refs[pi]);
} else {
ret_ty = semaTypeOf(sema, arg_refs[pi]);
}
break;
}
pi++;
@@ -4278,12 +4333,6 @@ static AirInstRef zirCall(
|| ret_ty == IP_INDEX_ENUM_LITERAL_TYPE);
bool is_inline_call
= func_info.is_inline || block->is_comptime || is_comptime_only_ret;
if (strcmp(dbg_callee, "copysign") == 0)
fprintf(stderr,
" COPYSIGN_PATH: is_inline=%d is_comptime=%d ct_only_ret=%d"
" ret_ty=%u is_generic=%d\n",
func_info.is_inline, block->is_comptime, is_comptime_only_ret,
ret_ty, is_generic);
if (!is_inline_call) {
// Dummy allocs for generic runtime params are now emitted
// interleaved in the arg evaluation loop above (Fix A).
@@ -4359,8 +4408,12 @@ static AirInstRef zirCall(
semaAddExtra(sema, runtime_arg_refs[a]);
// Ported from Sema.zig:7480: AstGen ensures a dbg_stmt
// always precedes a call instruction. For cross-module
// calls, inst-1 is in the calling module's ZIR.
// always precedes a call instruction. Use the caller's ZIR
// for cross-module calls. Note: the general dbg_stmt at the
// top of zirCall (before arg evaluation) may have been
// followed by arg instructions, so emit another one here
// to directly precede the call. zirDbgStmt de-duplicates
// when the previous block instruction is already a dbg_stmt.
if (is_cross_module) {
Zir temp = sema->code;
sema->code = saved_code;
@@ -4440,6 +4493,18 @@ static AirInstRef zirCall(
}
}
// Ported from src/Sema.zig line 7480: emit dbg_stmt for inline
// calls, just before the inline block. Use the caller's ZIR
// for cross-module calls.
if (inst > 0 && !block->is_comptime) {
Zir dbg_code = sema->code;
if (is_cross_module)
sema->code = saved_code;
zirDbgStmt(sema, block, inst - 1);
if (is_cross_module)
sema->code = dbg_code;
}
// Upstream: need_debug_scope = !block.isComptime() && ...
// When comptime (or all args comptime), use BLOCK; otherwise
// DBG_INLINE_BLOCK.
@@ -5884,11 +5949,11 @@ static InternPoolIndex resolveUnsignedIntType(InternPool* ip, uint32_t bits) {
}
}
// zirStructInitComptime: handle struct_init in comptime context.
// For @Type(.{.int = .{.signedness = .unsigned, .bits = N}}):
// - inner struct: extract signedness and bits, track as CT_TAG_INT_INFO
// - outer struct: extract field name (.int), track as CT_TAG_REIFY_INT
static AirInstRef zirStructInitComptime(Sema* sema, uint32_t inst) {
// zirStructInit: handle struct_init ZIR instruction.
// Comptime case: @Type(.{.int = .{.signedness = .unsigned, .bits = N}})
// Runtime case: emits AIR_INST_AGGREGATE_INIT with field values.
// Ported from src/Sema.zig zirStructInit / finishStructInit.
static AirInstRef zirStructInit(Sema* sema, SemaBlock* block, uint32_t inst) {
uint32_t payload_index = sema->code.inst_datas[inst].pl_node.payload_index;
// StructInit payload: abs_node, abs_line, fields_len, then Items.
uint32_t fields_len = sema->code.extra[payload_index + 2];
@@ -5967,7 +6032,32 @@ static AirInstRef zirStructInitComptime(Sema* sema, uint32_t inst) {
return AIR_REF_FROM_IP(marker);
}
return AIR_REF_FROM_IP(IP_INDEX_VOID_VALUE);
// Runtime struct init: emit aggregate_init.
// Ported from Sema.zig finishStructInit line 19655.
// The field_type instructions provide the container type.
// Resolve the container type from the first field_type's container ref.
{
uint32_t ft_payload0
= sema->code.inst_datas[items[0]].pl_node.payload_index;
ZirInstRef container_ref = sema->code.extra[ft_payload0];
AirInstRef container_air = resolveInst(sema, container_ref);
TypeIndex container_ty = AIR_REF_IS_IP(container_air)
? AIR_REF_TO_IP(container_air)
: IP_INDEX_VOID_TYPE;
// Build aggregate_init extra: element refs in field order.
uint32_t extra_idx = sema->air_extra_len;
for (uint32_t f = 0; f < fields_len; f++) {
ZirInstRef init_ref2 = items[f * 2 + 1];
AirInstRef init_air2 = resolveInst(sema, init_ref2);
semaAddExtra(sema, init_air2);
}
AirInstData data;
memset(&data, 0, sizeof(data));
data.ty_pl.ty_ref = AIR_REF_FROM_IP(container_ty);
data.ty_pl.payload = extra_idx;
return semaAddInst(block, AIR_INST_AGGREGATE_INIT, data);
}
}
// zirReifyComptime: handle @Type(...) in comptime context.
@@ -6629,6 +6719,10 @@ static bool analyzeBodyInner(
zirArithmetic(sema, block, inst, AIR_INST_MUL_WRAP));
i++;
continue;
case ZIR_INST_DIV:
instMapPut(&sema->inst_map, inst, zirDiv(sema, block, inst));
i++;
continue;
case ZIR_INST_ADD_SAT:
instMapPut(&sema->inst_map, inst,
zirArithmetic(sema, block, inst, AIR_INST_ADD_SAT));
@@ -6724,8 +6818,18 @@ static bool analyzeBodyInner(
instMapPut(&sema->inst_map, inst,
AIR_REF_FROM_IP(ipIntern(sema->ip, key)));
} else {
// Runtime: emit AIR_INST_MAX or AIR_INST_MIN.
TypeIndex peer_ty
= semaResolvePeerTypes(sema, min_lhs, min_rhs);
AirInstRef c_lhs = semaCoerce(sema, block, peer_ty, min_lhs);
AirInstRef c_rhs = semaCoerce(sema, block, peer_ty, min_rhs);
AirInstData data;
memset(&data, 0, sizeof(data));
data.bin_op.lhs = c_lhs;
data.bin_op.rhs = c_rhs;
instMapPut(&sema->inst_map, inst,
AIR_REF_FROM_IP(IP_INDEX_VOID_VALUE));
semaAddInst(
block, is_max ? AIR_INST_MAX : AIR_INST_MIN, data));
}
i++;
continue;
@@ -6860,9 +6964,16 @@ static bool analyzeBodyInner(
i++;
continue;
// @abs.
case ZIR_INST_ABS:
instMapPut(&sema->inst_map, inst, zirAbs(sema, block, inst));
i++;
continue;
// Validation-only (no AIR emitted).
case ZIR_INST_VALIDATE_DEREF:
case ZIR_INST_VALIDATE_CONST:
case ZIR_INST_VALIDATE_REF_TY:
i++;
continue;
@@ -7078,10 +7189,10 @@ static bool analyzeBodyInner(
i++;
continue;
// struct_init: comptime struct initialization.
// struct_init: struct initialization (comptime or runtime).
case ZIR_INST_STRUCT_INIT:
instMapPut(
&sema->inst_map, inst, zirStructInitComptime(sema, inst));
&sema->inst_map, inst, zirStructInit(sema, block, inst));
i++;
continue;
@@ -7676,8 +7787,6 @@ static bool analyzeBodyInner(
}
semaAddExtra(sema, sub_br_idx);
sema->air_inst_tags[br] = AIR_INST_BLOCK;
fprintf(
stderr, " REWRITE_BR_BLOCK: air_idx=%u\n", br);
sema->air_inst_datas[br].ty_pl.ty_ref
= AIR_REF_FROM_IP(resolved_ty);
sema->air_inst_datas[br].ty_pl.payload = sub_extra;
@@ -8127,7 +8236,6 @@ static bool analyzeBodyInner(
// For all other instructions, produce a void mapping and skip.
// As handlers are implemented, they will replace this default.
default: {
fprintf(stderr, " UNHANDLED: inst=%u tag=%u\n", inst, inst_tag);
AirInstRef air_ref = AIR_REF_FROM_IP(IP_INDEX_VOID_TYPE);
instMapPut(&sema->inst_map, inst, air_ref);
i++;

View File

@@ -456,6 +456,7 @@ fn airDataRefSlots(tag_val: u8) [2]bool {
c.AIR_INST_STRUCT_FIELD_PTR,
c.AIR_INST_DBG_INLINE_BLOCK,
c.AIR_INST_BLOCK,
c.AIR_INST_AGGREGATE_INIT,
=> .{ true, false },
// bin_op: lhs(Ref) + rhs(Ref)
c.AIR_INST_ADD,
@@ -488,6 +489,19 @@ fn airDataRefSlots(tag_val: u8) [2]bool {
c.AIR_INST_CMP_GTE,
c.AIR_INST_CMP_GT,
c.AIR_INST_CMP_NEQ,
c.AIR_INST_MAX,
c.AIR_INST_MIN,
c.AIR_INST_DIV_FLOAT,
c.AIR_INST_DIV_FLOAT_OPTIMIZED,
c.AIR_INST_DIV_TRUNC,
c.AIR_INST_DIV_TRUNC_OPTIMIZED,
c.AIR_INST_DIV_FLOOR,
c.AIR_INST_DIV_FLOOR_OPTIMIZED,
c.AIR_INST_DIV_EXACT,
c.AIR_INST_DIV_EXACT_OPTIMIZED,
c.AIR_INST_ADD_SAT,
c.AIR_INST_SUB_SAT,
c.AIR_INST_MUL_SAT,
=> .{ true, true },
// ty_op: type(Ref) + operand(Ref)
c.AIR_INST_BITCAST,
@@ -515,6 +529,7 @@ fn airDataRefSlots(tag_val: u8) [2]bool {
c.AIR_INST_CTZ,
c.AIR_INST_POPCOUNT,
c.AIR_INST_BYTE_SWAP,
c.AIR_INST_ABS,
c.AIR_INST_STRUCT_FIELD_PTR_INDEX_0,
c.AIR_INST_STRUCT_FIELD_PTR_INDEX_1,
c.AIR_INST_STRUCT_FIELD_PTR_INDEX_2,
@@ -1342,3 +1357,19 @@ test "sema air: inline fn with += call inside two conditionals" {
\\}
);
}
test "sema air: @abs float" {
try semaAirRawCheck("export fn f(x: f64) f64 { return @abs(x); }");
}
test "sema air: @max float" {
try semaAirRawCheck("export fn f(x: f64, y: f64) f64 { return @max(x, y); }");
}
test "sema air: @min float" {
try semaAirRawCheck("export fn f(x: f64, y: f64) f64 { return @min(x, y); }");
}
test "sema air: f64 div" {
try semaAirRawCheck("export fn f(x: f64, y: f64) f64 { return x / y; }");
}

View File

@@ -164,7 +164,7 @@ const corpus_files = .{
"../lib/compiler_rt/mulhc3.zig", // 425
"../lib/compiler_rt/mulsc3.zig", // 425
"../lib/compiler_rt/mulxc3.zig", // 425
//"../lib/compiler_rt/divdc3.zig", // 434
"../lib/compiler_rt/divdc3.zig", // 434
//"../lib/compiler_rt/divhc3.zig", // 434
//"../lib/compiler_rt/divsc3.zig", // 434
//"../lib/compiler_rt/divxc3.zig", // 434