Skip to content

c: __builtin_unreachable() as an operand of a larger expression still emits rows after UNREACHABLE (sink((__builtin_unreachable(), x));) — invalid IR by default, rejected under -fno-frontend-ssa #1748

Description

@davidgmbb

Summary

#1682 and #1743 (closing #1503 and #1736) keep the block open after a noreturn call until the value's consumer, or the whole expression statement, has emitted its rows. __builtin_unreachable() does not go through that path. It always ends the block at once, so the rest of the enclosing expression is emitted behind the UNREACHABLE.

The C below is valid, and Clang 18.1.3 and GCC 13.3.0 accept it with -fsyntax-only -Wall:

void sink(int);
int arg(int x)    { sink((__builtin_unreachable(), x)); return x; }
int init(int x)   { int y = (__builtin_unreachable(), x + 1); return y; }
int assign(int x) { x = (__builtin_unreachable(), 0); return x; }
int ret(int x)    { return (__builtin_unreachable(), x); }
int stmt(int x)   { if (x) __builtin_unreachable(); return x; }   /* control: statement level */

Observed on main 126cc437e5935f2c829780597c0a8f67e203a22d

Function default -fverify-codegen -fno-frontend-ssa (no verify)
arg compiles, no diagnostic canonical input, error 3, … instruction 3, opcode 18 local-promotion output, error 3, … opcode 18
init compiles, no diagnostic canonical input, error 3, … opcode 14 local-promotion output, error 3, … opcode 14
assign compiles, no diagnostic canonical input, error 3, … opcode 14 local-promotion output, error 3, … opcode 14
ret unsupported C function-body statement or expression near 'x' same same
stmt compiles passes compiles

Cause

The __builtin_unreachable branch of the prepared-call loop (c_gen.c:20646–20660 at 126cc43) appends UNREACHABLE and stores terminated = true unconditionally:

IrInstruction unreachable = c_ir_instruction_initialize(IR_OPCODE_UNREACHABLE, builder->void_type);
c_ir_append_instruction(builder, unreachable, unreachable_source);
builder->function->blocks[builder->current_block.value].terminated = true;

The noreturn-call path goes through c_ir_end_control_flow_after_call, which asks c_ir_lowering_branch_resumes_after_call whether a consumer resumes after the value, and defers the close to the end of an expression statement (#1743). The builtin branch skips both. Routing it through c_ir_end_control_flow_after_call(builder, true, unreachable_source) would probably fix the first three rows. That is untested; the #1325 branch noted on #1503 made that same change.

Known fix, with measurements

#1453 merged with this main (local merge commit 81172a59) handles the cases like this:

Done when

Environment

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions