On Thu, Jun 19, 2025 at 12:13 PM Mikael Morin <morin-mik...@orange.fr> wrote: > > 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.
Fair enough - most builtins should be handled via constant-folding, you should be able to use gimple_fold_stmt_to_constant with providing a valueization hook that will ask you for the value of SSA names referenced, so if you keep a simple lattice this should work for most. Richard. >