predicate-conditional-aggregate-box

Boxed aggregate in a predicated conditional expression

Relocating a delayed conditional expression into a reference for a predicate check leaves its boxed aggregates unexpanded.

AcceptedApplies standaloneSince 1.2.0

Where it applies.

How each patchset treats this bundle on each GCC major
PatchsetGCC 13GCC 14GCC 15GCC 16
1.2.0 (latest)Known-good control13.2.0Known-good control14.2.0Patchedgcc-15-1615.3.0Patchedgcc-15-1616.2.0
1.1.0Not applicable13.2.0Not applicable14.2.0Not applicable15.3.0Not applicable16.2.0
1.0.1Not applicable13.2.0Not applicable14.2.0Not applicable15.3.0Not applicable16.1.0
1.0.0Not applicable13.2.0Not applicable14.2.0Not applicable15.3.0Not applicable16.1.0

Explanation.

A record aggregate that contains a box is expanded top down by its parent construct. Convert_To_Assignments sets Expansion_Delayed on it, and since GCC 15 Delay_Conditional_Expressions_Between propagates that delay to every conditional expression between the aggregate and the parent that will drive the expansion, which here is a component association of the enclosing aggregate.

When the inner record type carries a checked Dynamic_Predicate, resolution of the enclosing aggregate applies the predicate check to that component first. The check duplicates the expression, so Remove_Side_Effects relocates the conditional expression into a standalone Rnn : constant Ann := <expr>'Reference; declaration and leaves Rnn.all behind. The parent that was going to drive the delayed expansion is no longer above the expression.

Remove_Side_Effects already handles precisely this for a delayed aggregate: when it builds the reference it clears Expansion_Delayed and Analyzed so the relocated aggregate is expanded in full. GCC 15 introduced delayed conditional expressions without extending that handling. The conditional expression and the boxed aggregates in its dependent expressions therefore keep their delayed flags, nothing ever expands them, and an aggregate reaches gnat_to_gnu, which asserts that no aggregate with delayed expansion can reach the code generator. The compiler aborts with a GNAT BUG DETECTED box reporting in gnat_to_gnu, at ada/gcc-interface/trans.cc and produces no object file.

The patch extends the existing delayed-aggregate handling in Remove_Side_Effects to delayed conditional expressions. It cancels the delay on the conditional expression, on the dependent aggregates, and on nested delayed conditional expressions, and schedules them for reanalysis. The relocated expression is then expanded exactly as the same code is without a predicate, and as GCC 13 and 14 already expand it. The predicate check itself is unchanged and still runs against the selected branch.

GCC 13 and 14 expand the branch aggregates before the predicate check relocates the conditional expression, so they compile and run the fixture unchanged and remain unpatched known-good controls.

patches/gcc-15-16.patch is canonical for the pinned 15.3.0, 16.1.0, and 16.2.0 baselines. The pinned FSF and Darwin exp_util.adb blobs are identical per GCC release, so the single patch is unambiguous for both source flavors; each hunk lands at a line offset because later releases moved surrounding code.

tests/predicate_conditional_aggregate_box.adb is a byte-identical runnable copy of the test added by the patch. It covers others => <> and a named Component => <>, a compile-time-known and a run-time condition, an if expression and a case expression, and an object declaration and an assignment statement. Every case checks that the box actually supplied the component default High => 25, so a correction that compiled but dropped the default fails. Two further cases select a branch that violates the predicate and require Assertion_Error, so a correction that suppressed the check also fails.

A qualified expression written around the conditional expression avoids the abort rather than reaching it. Replacing the fixture's unqualified conditional with Policy'(if Flag then ... else ...) compiles on unpatched GCC 15.3.0 and 16.1.0, where the unqualified form aborts; both were measured on aarch64-apple-darwin. Why qualification changes the expansion has not been traced through the sources, so it is recorded here as an observed caller-side workaround and nothing more, alongside naming every component instead of using a box and hoisting the conditional expression into its own constant. It is not a separate defect and needs no separate bundle.

The defect is in the target-independent front end, and it was measured on aarch64-apple-darwin only: GCC 13.2.0, 14.1.0, and 14.2.0 compile and run the fixture, and GCC 15.0.1, 15.3.0, 16.1.0, and 16.2.0 abort. No Linux measurement was taken before this bundle was written; the Linux x86-64 and AArch64 lanes of the patchset are its first Linux measurement.

Patch.

Variant gcc-15-16

Applies to 15.3.0, 16.1.0, 16.2.0. Source flavors: linux, darwin_arm64.

+245 −0 2 files

Download the patch

gcc/ada/exp_util.adb +88−0modified
Unified diff for gcc/ada/exp_util.adb: original line, patched line, change, source
@@ -12344 +12344 @@
1234412344 -- that other side effects related to volatile variables are handled
1234512345 -- separately.
1234612346
12347Added line. procedure Undelay_Conditional_Expression (N : Node_Id);
12348Added line. -- N is a conditional expression whose expansion has been delayed,
12349Added line. -- possibly wrapped in qualified expressions. Cancel the delay for N
12350Added line. -- and for its dependent expressions, and schedule them for reanalysis.
12351Added line.
1234712352 ---------------------
1234812353 -- Build_Temporary --
1234912354 ---------------------
@@ -12467 +12472 @@
1246712472 end if;
1246812473 end Possible_Side_Effect_In_SPARK;
1246912474
12475Added line. ------------------------------------
12476Added line. -- Undelay_Conditional_Expression --
12477Added line. ------------------------------------
12478Added line.
12479Added line. procedure Undelay_Conditional_Expression (N : Node_Id) is
12480Added line.
12481Added line. procedure Undelay_Dependent_Expression (Expr : Node_Id);
12482Added line. -- Cancel the delayed expansion of the dependent expression Expr
12483Added line.
12484Added line. ----------------------------------
12485Added line. -- Undelay_Dependent_Expression --
12486Added line. ----------------------------------
12487Added line.
12488Added line. procedure Undelay_Dependent_Expression (Expr : Node_Id) is
12489Added line. begin
12490Added line. if Is_Delayed_Aggregate (Expr) then
12491Added line. if Nkind (Expr) = N_Qualified_Expression then
12492Added line. Set_Expansion_Delayed (Expression (Expr), False);
12493Added line. Set_Analyzed (Expression (Expr), False);
12494Added line. else
12495Added line. Set_Expansion_Delayed (Expr, False);
12496Added line. end if;
12497Added line.
12498Added line. Set_Analyzed (Expr, False);
12499Added line.
12500Added line. elsif Is_Delayed_Conditional_Expression (Expr) then
12501Added line. Undelay_Conditional_Expression (Expr);
12502Added line. end if;
12503Added line. end Undelay_Dependent_Expression;
12504Added line.
12505Added line. -- Local variables
12506Added line.
12507Added line. Alt : Node_Id;
12508Added line. Expr : Node_Id := N;
12509Added line.
12510Added line. -- Start of processing for Undelay_Conditional_Expression
12511Added line.
12512Added line. begin
12513Added line. -- Is_Delayed_Conditional_Expression looks through qualified
12514Added line. -- expressions surrounding conditional expressions, so we need to
12515Added line. -- reset the Analyzed flag on them as well.
12516Added line.
12517Added line. loop
12518Added line. Set_Analyzed (Expr, False);
12519Added line.
12520Added line. exit when Nkind (Expr) in N_Case_Expression | N_If_Expression;
12521Added line.
12522Added line. pragma Assert (Nkind (Expr) = N_Qualified_Expression);
12523Added line. Expr := Expression (Expr);
12524Added line. end loop;
12525Added line.
12526Added line. Set_Expansion_Delayed (Expr, False);
12527Added line.
12528Added line. -- The dependent expressions have been delayed along with the
12529Added line. -- conditional expression itself, so they must be undelayed too.
12530Added line.
12531Added line. if Nkind (Expr) = N_If_Expression then
12532Added line. Alt := Next (First (Expressions (Expr)));
12533Added line.
12534Added line. while Present (Alt) loop
12535Added line. Undelay_Dependent_Expression (Alt);
12536Added line. Next (Alt);
12537Added line. end loop;
12538Added line.
12539Added line. else
12540Added line. Alt := First (Alternatives (Expr));
12541Added line.
12542Added line. while Present (Alt) loop
12543Added line. Undelay_Dependent_Expression (Expression (Alt));
12544Added line. Next (Alt);
12545Added line. end loop;
12546Added line. end if;
12547Added line. end Undelay_Conditional_Expression;
12548Added line.
1247012549 -- Local variables
1247112550
1247212551 Loc : constant Source_Ptr := Sloc (Exp);
@@ -12875 +12954 @@
1287512954 end if;
1287612955
1287712956 Set_Analyzed (E, False);
12957Added line.
12958Added line. -- The expansion of a conditional expression is likewise delayed
12959Added line. -- when one of its dependent expressions is a nested aggregate, and
12960Added line. -- is then driven by the parent construct. The reference built just
12961Added line. -- above is not such a construct, so the conditional expression and
12962Added line. -- its dependent expressions must be fully expanded here as well.
12963Added line.
12964Added line. elsif Is_Delayed_Conditional_Expression (E) then
12965Added line. Undelay_Conditional_Expression (E);
1287812966 end if;
1287912967
1288012968 Insert_Action (Exp,
gcc/testsuite/gnat.dg/predicate_conditional_aggregate_box.adb +157−0new file
Unified diff for gcc/testsuite/gnat.dg/predicate_conditional_aggregate_box.adb: original line, patched line, change, source
@@ -0 +1 @@
1Added line. -- { dg-do run }
2Added line. -- { dg-options "-O2 -gnat2022" }
3Added line.
4Added line. pragma Assertion_Policy (Dynamic_Predicate => Check);
5Added line.
6Added line. with Ada.Assertions; use Ada.Assertions;
7Added line. with Ada.Text_IO; use Ada.Text_IO;
8Added line.
9Added line. procedure Predicate_Conditional_Aggregate_Box is
10Added line.
11Added line. type Policy is record
12Added line. Low : Natural := 1;
13Added line. High : Natural := 25;
14Added line. end record
15Added line. with Dynamic_Predicate => Policy.High >= Policy.Low;
16Added line.
17Added line. type Config is record
18Added line. Watch : Policy;
19Added line. end record;
20Added line.
21Added line. Static_Flag : constant Boolean := True;
22Added line.
23Added line. Enforced : Natural := 0;
24Added line. -- Number of predicate violations reported at run time
25Added line.
26Added line. function Ident (Value : Boolean) return Boolean;
27Added line. -- Identity function, used to keep the condition of a conditional
28Added line. -- expression from being known at compile time.
29Added line.
30Added line. procedure Check (Label : String; Actual, Expected : Natural);
31Added line. -- Report a mismatch between an observed and an expected component value
32Added line.
33Added line. procedure Verify (C : Config; Label : String; Low, High : Natural);
34Added line. -- Check both components of the inner record of C
35Added line.
36Added line. -----------
37Added line. -- Check --
38Added line. -----------
39Added line.
40Added line. procedure Check (Label : String; Actual, Expected : Natural) is
41Added line. begin
42Added line. if Actual /= Expected then
43Added line. raise Program_Error with
44Added line. Label & " is" & Natural'Image (Actual)
45Added line. & " instead of" & Natural'Image (Expected);
46Added line. end if;
47Added line. end Check;
48Added line.
49Added line. -----------
50Added line. -- Ident --
51Added line. -----------
52Added line.
53Added line. function Ident (Value : Boolean) return Boolean is
54Added line. begin
55Added line. return Value;
56Added line. end Ident;
57Added line.
58Added line. ------------
59Added line. -- Verify --
60Added line. ------------
61Added line.
62Added line. procedure Verify (C : Config; Label : String; Low, High : Natural) is
63Added line. begin
64Added line. Check (Label & " low", C.Watch.Low, Low);
65Added line. Check (Label & " high", C.Watch.High, High);
66Added line. end Verify;
67Added line.
68Added line. begin
69Added line. -- A box in a branch of a conditional expression that is a component
70Added line. -- association of an enclosing aggregate must supply the component
71Added line. -- defaults of the inner record type, here High => 25.
72Added line.
73Added line. -- Static condition, others box, object declaration
74Added line.
75Added line. declare
76Added line. C : constant Config :=
77Added line. (Watch => (if Static_Flag then (Low => 1, others => <>)
78Added line. else (Low => 2, others => <>)));
79Added line. begin
80Added line. Verify (C, "static others box", Low => 1, High => 25);
81Added line. end;
82Added line.
83Added line. -- Dynamic condition, named component box, object declaration
84Added line.
85Added line. declare
86Added line. C : constant Config :=
87Added line. (Watch => (if Ident (True) then (Low => 3, High => <>)
88Added line. else (Low => 4, High => <>)));
89Added line. begin
90Added line. Verify (C, "dynamic named box", Low => 3, High => 25);
91Added line. end;
92Added line.
93Added line. -- Dynamic condition, others box, assignment statement
94Added line.
95Added line. declare
96Added line. C : Config;
97Added line. begin
98Added line. C := (Watch => (if Ident (False) then (Low => 5, others => <>)
99Added line. else (Low => 6, others => <>)));
100Added line. Verify (C, "assigned others box", Low => 6, High => 25);
101Added line. end;
102Added line.
103Added line. -- Dynamic condition, named component box, assignment statement
104Added line.
105Added line. declare
106Added line. C : Config;
107Added line. begin
108Added line. C := (Watch => (if Ident (True) then (Low => 7, High => <>)
109Added line. else (Low => 8, High => <>)));
110Added line. Verify (C, "assigned named box", Low => 7, High => 25);
111Added line. end;
112Added line.
113Added line. -- Case expression, others box, object declaration
114Added line.
115Added line. declare
116Added line. C : constant Config :=
117Added line. (Watch => (case Ident (True) is
118Added line. when True => (Low => 9, others => <>),
119Added line. when False => (Low => 10, others => <>)));
120Added line. begin
121Added line. Verify (C, "case others box", Low => 9, High => 25);
122Added line. end;
123Added line.
124Added line. -- The predicate must still be enforced on the selected branch, in an
125Added line. -- object declaration and in an assignment statement alike. The defaulted
126Added line. -- High is 25, so a Low above it violates Policy.High >= Policy.Low.
127Added line.
128Added line. begin
129Added line. declare
130Added line. C : constant Config :=
131Added line. (Watch => (if Ident (True) then (Low => 30, others => <>)
132Added line. else (Low => 2, others => <>)));
133Added line. begin
134Added line. Verify (C, "unenforced declaration", Low => 30, High => 25);
135Added line. end;
136Added line. exception
137Added line. when Assertion_Error =>
138Added line. Enforced := Enforced + 1;
139Added line. end;
140Added line.
141Added line. begin
142Added line. declare
143Added line. C : Config;
144Added line. begin
145Added line. C := (Watch => (if Ident (False) then (Low => 1, others => <>)
146Added line. else (Low => 40, High => <>)));
147Added line. Verify (C, "unenforced assignment", Low => 40, High => 25);
148Added line. end;
149Added line. exception
150Added line. when Assertion_Error =>
151Added line. Enforced := Enforced + 1;
152Added line. end;
153Added line.
154Added line. Check ("enforced predicate violations", Enforced, 2);
155Added line.
156Added line. Put_Line ("PASS predicate conditional aggregate box");
157Added line. end Predicate_Conditional_Aggregate_Box;
158

Tests.

predicate_conditional_aggregate_box.adb Ada · 157 lines
--  { dg-do run }
--  { dg-options "-O2 -gnat2022" }

pragma Assertion_Policy (Dynamic_Predicate => Check);

with Ada.Assertions; use Ada.Assertions;
with Ada.Text_IO;    use Ada.Text_IO;

procedure Predicate_Conditional_Aggregate_Box is

   type Policy is record
      Low  : Natural := 1;
      High : Natural := 25;
   end record
     with Dynamic_Predicate => Policy.High >= Policy.Low;

   type Config is record
      Watch : Policy;
   end record;

   Static_Flag : constant Boolean := True;

   Enforced : Natural := 0;
   --  Number of predicate violations reported at run time

   function Ident (Value : Boolean) return Boolean;
   --  Identity function, used to keep the condition of a conditional
   --  expression from being known at compile time.

   procedure Check (Label : String; Actual, Expected : Natural);
   --  Report a mismatch between an observed and an expected component value

   procedure Verify (C : Config; Label : String; Low, High : Natural);
   --  Check both components of the inner record of C

   -----------
   -- Check --
   -----------

   procedure Check (Label : String; Actual, Expected : Natural) is
   begin
      if Actual /= Expected then
         raise Program_Error with
           Label & " is" & Natural'Image (Actual)
             & " instead of" & Natural'Image (Expected);
      end if;
   end Check;

   -----------
   -- Ident --
   -----------

   function Ident (Value : Boolean) return Boolean is
   begin
      return Value;
   end Ident;

   ------------
   -- Verify --
   ------------

   procedure Verify (C : Config; Label : String; Low, High : Natural) is
   begin
      Check (Label & " low", C.Watch.Low, Low);
      Check (Label & " high", C.Watch.High, High);
   end Verify;

begin
   --  A box in a branch of a conditional expression that is a component
   --  association of an enclosing aggregate must supply the component
   --  defaults of the inner record type, here High => 25.

   --  Static condition, others box, object declaration

   declare
      C : constant Config :=
        (Watch => (if Static_Flag then (Low => 1, others => <>)
                                  else (Low => 2, others => <>)));
   begin
      Verify (C, "static others box", Low => 1, High => 25);
   end;

   --  Dynamic condition, named component box, object declaration

   declare
      C : constant Config :=
        (Watch => (if Ident (True) then (Low => 3, High => <>)
                                   else (Low => 4, High => <>)));
   begin
      Verify (C, "dynamic named box", Low => 3, High => 25);
   end;

   --  Dynamic condition, others box, assignment statement

   declare
      C : Config;
   begin
      C := (Watch => (if Ident (False) then (Low => 5, others => <>)
                                       else (Low => 6, others => <>)));
      Verify (C, "assigned others box", Low => 6, High => 25);
   end;

   --  Dynamic condition, named component box, assignment statement

   declare
      C : Config;
   begin
      C := (Watch => (if Ident (True) then (Low => 7, High => <>)
                                      else (Low => 8, High => <>)));
      Verify (C, "assigned named box", Low => 7, High => 25);
   end;

   --  Case expression, others box, object declaration

   declare
      C : constant Config :=
        (Watch => (case Ident (True) is
                      when True  => (Low => 9, others => <>),
                      when False => (Low => 10, others => <>)));
   begin
      Verify (C, "case others box", Low => 9, High => 25);
   end;

   --  The predicate must still be enforced on the selected branch, in an
   --  object declaration and in an assignment statement alike. The defaulted
   --  High is 25, so a Low above it violates Policy.High >= Policy.Low.

   begin
      declare
         C : constant Config :=
           (Watch => (if Ident (True) then (Low => 30, others => <>)
                                      else (Low => 2, others => <>)));
      begin
         Verify (C, "unenforced declaration", Low => 30, High => 25);
      end;
   exception
      when Assertion_Error =>
         Enforced := Enforced + 1;
   end;

   begin
      declare
         C : Config;
      begin
         C := (Watch => (if Ident (False) then (Low => 1, others => <>)
                                          else (Low => 40, High => <>)));
         Verify (C, "unenforced assignment", Low => 40, High => 25);
      end;
   exception
      when Assertion_Error =>
         Enforced := Enforced + 1;
   end;

   Check ("enforced predicate violations", Enforced, 2);

   Put_Line ("PASS predicate conditional aggregate box");
end Predicate_Conditional_Aggregate_Box;

Download · View in repository

run-test.sh shell · 71 lines
#!/usr/bin/env bash
set -euo pipefail

if [[ $# -ne 3 ]]; then
  echo "usage: $0 TOOLCHAIN_ROOT GCC_VERSION unpatched|patched" >&2
  exit 2
fi

root=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
source "$root/scripts/regression-common.sh"
resolve_regression_toolchain "$1"
version=$2
state=$3
[[ "$state" == unpatched || "$state" == patched ]] || {
  echo "error: state must be unpatched or patched" >&2
  exit 2
}

# GCC 13 and 14 expand the inner aggregate before the predicate check relocates
# the conditional expression, so they compile and run the fixture unchanged and
# are unpatched known-good controls.
control=no
case "$version" in
  13.2.0 | 14.2.0) control=yes ;;
esac

fixture="$root/bundles/predicate-conditional-aggregate-box/tests/predicate_conditional_aggregate_box.adb"
work=$(mktemp -d "${TMPDIR:-/tmp}/gnat-predicate-conditional-test.XXXXXX")
trap 'rm -rf "$work"' EXIT

for optimization in 0 2; do
  case_dir="$work/O$optimization"
  mkdir -p "$case_dir"
  cp "$fixture" "$case_dir/predicate_conditional_aggregate_box.adb"
  set +e
  (
    cd "$case_dir"
    "${REGRESSION_ENV[@]}" "$REGRESSION_GNATMAKE" -q -f -gnat2022 \
      "-O$optimization" predicate_conditional_aggregate_box.adb
  ) >"$case_dir/build.log" 2>&1
  build_status=$?
  set -e

  if [[ "$control" == no && "$state" == unpatched ]]; then
    [[ $build_status -ne 0 ]] || {
      echo "error: unpatched predicate-conditional regression unexpectedly compiled at -O$optimization" >&2
      exit 1
    }
    if ! grep -Fq 'GNAT BUG DETECTED' "$case_dir/build.log" ||
       ! grep -Fq 'in gnat_to_gnu' "$case_dir/build.log"; then
      cat "$case_dir/build.log"
      exit 1
    fi
    echo "predicate-conditional-aggregate-box -O$optimization: expected front-end abort (GCC $version)"
    continue
  fi

  [[ $build_status -eq 0 ]] || { cat "$case_dir/build.log"; exit 1; }
  "${REGRESSION_ENV[@]}" "$case_dir/predicate_conditional_aggregate_box" \
    >"$case_dir/output.log" 2>&1 || {
      cat "$case_dir/output.log"
      exit 1
    }
  grep -F "PASS predicate conditional aggregate box" "$case_dir/output.log"
  if [[ "$control" == yes ]]; then
    expected=known-good-control
  else
    expected=patched
  fi
  echo "predicate-conditional-aggregate-box -O$optimization: $expected (GCC $version)"
done

Download · View in repository

Commands.

Apply the patch
patch --fuzz=0 -p1 -i bundles/predicate-conditional-aggregate-box/patches/gcc-15-16.patch
Build the compiler
PATH=/path/to/bootstrap/bin:$PATH ./scripts/build-gnat.sh SOURCE BUILD INSTALL
Run the regression
./scripts/run-regressions.sh INSTALL 1.2.0 GCC_MAJOR patched

Metadata.