https://gcc.gnu.org/bugzilla/show_bug.cgi?id=127287
Bug ID: 127287
Summary: [c++26][contracts] ICE in expand_expr_real_1 when a
lambda's own contract specifier names its parameter
and the lambda is instantiated from a template
Product: gcc
Version: 16.2.0
Status: UNCONFIRMED
Severity: normal
Priority: P3
Component: c++
Assignee: unassigned at gcc dot gnu.org
Reporter: berne at notadragon dot com
Target Milestone: ---
Created attachment 65534
--> https://gcc.gnu.org/bugzilla/attachment.cgi?id=65534&action=edit
ICE in expand_expr_real_1, with the three necessary ingredients listed
A lambda with a contract specifier whose predicate names the lambda's
own parameter ICEs when the lambda is instantiated as part of a template.
```
template <class T>
int ft(T a)
{
auto inner = [](int b) pre(b > 0) { return b; };
return inner(a);
}
int f() { return ft(1); }
```
```
$ ./gcc-16.2.0/bin/g++ -std=c++26 -c \
contract-lambda-in-template-ice.cpp
during RTL pass: expand
contract-lambda-in-template-ice.cpp: In lambda function:
contract-lambda-in-template-ice.cpp:15:18: internal compiler error: in
expand_expr_real_1, at expr.cc:11648
15 | auto inner = [](int b) pre(b > 0) { return b; };
| ^
0x74ee6602a1c9 __libc_start_call_main
../sysdeps/nptl/libc_start_call_main.h:58
0x74ee6602a28a __libc_start_main_impl
../csu/libc-start.c:360
```
>From a trunk build (7dab38c9d71) configured the same way, but with -g, we
get a more complete stack trace:
```
0x24e67c5 internal_error(char const*, ...)
/workarea/gnu_gcc_trunk/gcc/diagnostic-global-context.cc:787
0x824eb1 fancy_abort(char const*, int, char const*)
/workarea/gnu_gcc_trunk/gcc/diagnostics/context.cc:1813
0x7ccda4 expand_expr_real_1(tree_node*, rtx_def*, machine_mode,
expand_modifier, rtx_def**, bool)
/workarea/gnu_gcc_trunk/gcc/expr.cc:11783
0xbed6c0 expand_normal(tree_node*)
/workarea/gnu_gcc_trunk/gcc/expr.h:329
0xbed6c0 do_compare_and_jump
/workarea/gnu_gcc_trunk/gcc/dojump.cc:1288
0xbee643 do_jump_1
/workarea/gnu_gcc_trunk/gcc/dojump.cc:242
0xb7b4f8 expand_gimple_cond
/workarea/gnu_gcc_trunk/gcc/cfgexpand.cc:3054
0xb7b4f8 expand_gimple_basic_block
/workarea/gnu_gcc_trunk/gcc/cfgexpand.cc:6395
0xb7cb7f execute
/workarea/gnu_gcc_trunk/gcc/cfgexpand.cc:7295
```
Each ingredient is necessary -- dropping any one compiles clean:
- the contract is a specifier on the lambda. A contract_assert in the
lambda's body is part of the body, substitutes with it, and is fine;
- the predicate names the lambda's own parameter. pre(true), or a
predicate naming only a global, is fine;
- the lambda is instantiated as part of a template. The same lambda in a
non-template function is fine, and so is one that is never called.
Neither nesting nor a generic lambda is required, though this was first
found inside one.
The same defect reached through a postcondition rather than a precondition
surfaces elsewhere, and only where tree checking is enabled:
```
template <class T> int ft(T a) {
auto inner = [](int b) post(r : r > 0) { return b; };
return inner((int)a);
}
int f() { return ft(1) + ft(2.0); }
```
That is accepted by 16.1.0 and 16.2.0 and by a trunk build configured
--enable-checking=release, and ICEs on a trunk build taking configure's
development default of --enable-checking=yes,extra. Two instantiations are
needed: the first silently reparents the pattern's result variable onto
itself, and the second trips over it. Though this hits an assertion
in checked builds, we were not able to find a case where the generated
code was corrupted by the inconsistent trees.
DISCOVERY
Found while implementing capture of enclosing entities by a lambda written
inside a contract predicate ([expr.prim.lambda.capture]/3.3, PR117435). The
matrix of shapes was first classified on a release build, where the second
face is compiled out -- a checking build contradicted the recorded table
within a minute of first being used.
ANALYSIS
tsubst_function_decl copies a function's contract specifiers onto the
instantiation without substituting them -- its own comment says so -- because
for an ordinary function regenerate_decl_from_template substitutes them
later. A lambda's operator() never reaches regenerate_decl_from_template:
tsubst_lambda_expr builds it and substitutes its body on the spot.
So the instantiation keeps the pattern's contract trees, whose predicate
names the pattern's PARM_DECLs and whose result binding belongs to the
pattern. Nothing detects this, so it surfaces wherever the stale tree is
first used -- which is why it looks like several unrelated bugs depending on
the shape and on the build's checking level.
VERSIONS -- all on x86_64-linux-gnu
source version ICE
compiler-explorer 16.1.0 yes (expr.cc:11648)
compiler-explorer 16.2.0 yes (expr.cc:11648)
compiler-explorer 17.0.0 20260909, 919c0d16c91 yes (expr.cc:11783)
local build -g 17.0.0 20260909, 7dab38c9d71 yes (expr.cc:11789)
The compiler-explorer trunk build takes configure's development default,
--enable-checking=yes,extra; the 16.x builds and the local build are
--enable-checking=release. That difference is what the second face above
depends on.
```
$ ./gcc-16.2.0/bin/g++ -v
Using built-in specs.
COLLECT_GCC=./gcc-16.2.0/bin/g++
COLLECT_LTO_WRAPPER=/home/jberne4/repos/compilers/gcc-16.2.0/bin/../libexec/gcc/x86_64-linux-gnu/16.2.0/lto-wrapper
Target: x86_64-linux-gnu
Configured with: ../gcc-16.2.0/configure
--prefix=/opt/compiler-explorer/gcc-build/staging --build=x86_64-linux-gnu
--host=x86_64-linux-gnu --target=x86_64-linux-gnu --disable-bootstrap
--enable-multiarch --with-abi=m64 --with-multilib-list=m32,m64,mx32
--enable-multilib --enable-clocale=gnu
--enable-languages=c,c++,fortran,ada,objc,obj-c++,go,d,m2,rust,cobol,algol68
--enable-ld=yes --enable-gold=yes --enable-libstdcxx-time=yes
--enable-linker-build-id --enable-lto --enable-plugins --enable-threads=posix
--with-pkgversion=Compiler-Explorer-Build-gcc--binutils-2.44
Thread model: posix
Supported LTO compression algorithms: zlib
gcc version 16.2.0 (Compiler-Explorer-Build-gcc--binutils-2.44)
```