Le 18/06/2025 à 16:51, Richard Biener a écrit :
On Wed, Jun 18, 2025 at 11:23 AM Mikael Morin <morin-mik...@orange.fr> wrote:

From: Mikael Morin <mik...@gcc.gnu.org>

Hello,

I'm proposing here an interpretor/simulator of the gimple IR.
It proved useful for me to debug complicated testcases, where
the misbehaviour is not obvious if you just look at the IR dump.
It produces an execution trace on the standard error stream, where
one can see the values of variables changing as statements are executed.

I only implemented the bits that were needed in my testcase(s), so there
are some holes in the implementation, especially regarding builtin
functions.

Here are two sample outputs:

a = {-2.0e+0, 3.0e+0, -5.0e+0, 7.0e+0, -1.1e+1, 1.3e+1};
   # a[0] = -2.0e+0
   # a[1] = 3.0e+0
   # a[2] = -5.0e+0
   # a[3] = 7.0e+0
   # a[4] = -1.1e+1
   # a[5] = 1.3e+1
b = {1.7e+1, -2.3e+1, 2.9e+1, -3.1e+1, 3.7e+1, -4.1e+1};
   # b[0] = 1.7e+1
   # b[1] = -2.3e+1
   # b[2] = 2.9e+1
   # b[3] = -3.1e+1
   # b[4] = 3.7e+1
   # b[5] = -4.1e+1
# Entering function main
   # Executing bb 0
   # Leaving bb 0, preparing to execute bb 2
   # Executing bb 2
   _gfortran_set_args (argc_1(D), argv_2(D));
     # ignored
   _gfortran_set_options (7, &options__7[0]);
     # ignored
   _3 = __builtin_calloc (12, 1);
     # _3 = &<alloc00(12)>
   if (_3 == 0B)
     # Condition evaluates to false
   # Leaving bb 2, preparing to execute bb 4
   __var_3_do_19 = PHI <0(2), _17(5)>
     # __var_3_do_19 = 0
   _18 = PHI <0.0(2), _5(5)>
     # _18 = 0.0
   # Executing bb 4
   _17 = __var_3_do_19 + 1;
     # _17 = 1
   _14 = (long unsigned int) _17;
     # _14 = 1
   _13 = MEM[(real__kind_4_ *)&a + -4B + _14 * 4];
     # _13 = -2.0e+0
   _12 = _13 * 2.9e+1;
     # _12 = -5.8e+1
   _11 = _12 + _18;
     # _11 = -5.8e+1
   MEM[(real__kind_4_ *)_3 + -4B + _14 * 4] = _11;
     # MEM[(real__kind_4_ *)_3 + -4B + _14 * 4] = -5.8e+1
   if (_17 == 3)
     # Condition evaluates to false
   # Leaving bb 4, preparing to execute bb 5
   # Executing bb 5
   _5 = MEM[(real__kind_4_ *)_3 + _14 * 4];
     # _5 = 0.0
   # Leaving bb 5, preparing to execute bb 4
   __var_3_do_19 = PHI <0(2), _17(5)>
     # __var_3_do_19 = 1
   _18 = PHI <0.0(2), _5(5)>
     # _18 = 0.0
   # Executing bb 4
   _17 = __var_3_do_19 + 1;
     # _17 = 2
   _14 = (long unsigned int) _17;
     # _14 = 2
   _13 = MEM[(real__kind_4_ *)&a + -4B + _14 * 4];
     # _13 = 3.0e+0
   _12 = _13 * 2.9e+1;
     # _12 = 8.7e+1
   _11 = _12 + _18;
     # _11 = 8.7e+1
   MEM[(real__kind_4_ *)_3 + -4B + _14 * 4] = _11;
     # MEM[(real__kind_4_ *)_3 + -4B + _14 * 4] = 8.7e+1
   if (_17 == 3)
     # Condition evaluates to false
   # Leaving bb 4, preparing to execute bb 5
   # Executing bb 5
   _5 = MEM[(real__kind_4_ *)_3 + _14 * 4];
     # _5 = 0.0
   # Leaving bb 5, preparing to execute bb 4
   __var_3_do_19 = PHI <0(2), _17(5)>
     # __var_3_do_19 = 2
   _18 = PHI <0.0(2), _5(5)>
     # _18 = 0.0
   # Executing bb 4
   ...

   MEM <vector(2) char> [(character__kind_1_ *)&str] = { 97, 99 };
     # str[0][0] = 97
     # str[1][0] = 99
   str[2][0] = 97;
     # str[2][0] = 97
   parm__3.data = &str;
     # parm__3.data = &str
   parm__3.offset = -1;
     # parm__3.offset = -1
   parm__3.dtype.elem_len = 1;
     # parm__3.dtype.elem_len = 1
   MEM <long unsigned int> [(void *)&parm__3 + 24B] = 6601364733952;
     # parm__3.dtype.version = 0
     # parm__3.dtype.rank = 1
     # parm__3.dtype.type = 6
     # parm__3.dtype.attribute = 0
   MEM <vector(2) long int> [(struct array01_character__kind_1_ *)&parm__3 + 
32B] = { 1, 1 };
     # parm__3.span = 1
     # parm__3.dim[0].spacing = 1
   MEM <vector(2) long int> [(struct array01_character__kind_1_ *)&parm__3 + 
48B] = { 1, 3 };
     # parm__3.dim[0].lbound = 1
     # parm__3.dim[0].ubound = 3
   atmp__4.offset = 0;
     # atmp__4.offset = 0
   atmp__4.dtype.elem_len = 4;
     # atmp__4.dtype.elem_len = 4
   MEM <long unsigned int> [(void *)&atmp__4 + 24B] = 1103806595072;
     # atmp__4.dtype.version = 0
     # atmp__4.dtype.rank = 1
     # atmp__4.dtype.type = 1
     # atmp__4.dtype.attribute = 0
   MEM <vector(2) long int> [(struct array01_integer__kind_4_ *)&atmp__4 + 32B] 
= { 4, 4 };
     # atmp__4.span = 4
     # atmp__4.dim[0].spacing = 4
   MEM <vector(2) long int> [(struct array01_integer__kind_4_ *)&atmp__4 + 48B] 
= { 0, 0 };
     # atmp__4.dim[0].lbound = 0
     # atmp__4.dim[0].ubound = 0
   atmp__4.data = &A__5;
     # atmp__4.data = &A__5
   ...

Is anyone interested in integrating this into mainline?
Thoughts, comments?

Nice.  Can you expand a bit on how you model global state?

Do you mean compiler global state?
Well, I don't know, I don't model anything, nor do I know what there would be to model.

If you mean in the user program, global variables and memory allocation are modelled. global variables are in a special root scope, that is parent of the main function scope; thus they are reachable from every function scope. Memory allocation is modelled the obvious way; new storage areas without variable attached to them are created dynamically as specific implementation of __builtin_malloc. It is up to the user program to keep track of the address and provide it to read or write in the storage area.

How do
you handle the case of an unknown value (see below, a symbolic
value might be a useful thing?)?

It's basically an interpreter, so most values are known. The two exceptions that are handled are uninitialised bits/variables, and pointers/addresses. Uninitialised bits are tracked and propagated on copy. Addresses are close to a symbolic value, they are represented as a reference to the storage area and an offset.

In that case external input
(when debugging an issue) would be useful.  Also for debugging running
the simulation after a specific pass would be nice - given duing main opts
not all functions are at the same pass position, it might be nice to be able
to run the simulation on GIMPLE IR as read from the GIMPLE frontend
(so you could -fdump-tree-<pass>-gimple and stitch together a harness).

Actually, I have used it together with the gimple frontend. The code is not mature enough to support every possible IL that may come out of any pass; the gimple frontend makes it possible to tweak the IL to circumvent some bugs or implementation holes easily.

Given you print an execution trace, and the pass issue, should this be
a dump modifier, aka -fdump-tree-<pass>-execution, and the trace amended
to the dump file (after the last function is processed, as said, there's no good
global state at all points)?

I'll notice that we have some bits of "interpretation" around in
constant-folding,
crc-verification.cc (which has a limited symbolic execution engine),
tree-ssa-loop-niter.cc (loop_niter_by_eval).  Some kind of common abstraction
that centralizes the semantic of a stmt would be nice to have.

The simulator uses some of the constant-folding functions to get many TREE_CODE values supported for free. Regarding crc-verification and niter, they seem to target a very specific problem, so I'm not sure it's worth it.

Reply via email to