Prozedur Beziehungen

Jeder, der sich in ein komplexeres Programmsystem einarbeitet, versucht spätestens, nachdem er alle Prozeduren einzeln betrachtet hat, herauszufinden, wie die Prozeduren untereinander zusammenhängen. Das kleinere Problem ist es, alle Prozeduren aufzustellen, die die gerade betrachtete Prozedur selbst aufruft.

Wesentlich schwieriger wird es, wenn man auch noch wissen möchte, wo die aktuelle Prozedur ihrerseits aufgerufen wird, was dann meistens auf einen fleißigen Gebrauch der vorhandenen Suchfunktionen des Editors hinausläuft. Weil diese Sucherei unnötig viel Zeit kostet, werden oft nur die interessantesten Stellen genauer untersucht und somit bleiben viele wichtige Details unbeachtet, was sich später vielleicht rächt. Abhilfe schafft ein Programm, das alle vorkommenden Bezüge selbständig erfaßt, so daß man auf einen Blick sehen kann, welche Prozeduren aufgerufen werden bzw. von welchen Prozeduren die aktuelle Prozedur aufgerufen wird. Sehr wichtig ist die Tatsache, daß man sich eine einmal erstellte Bezugsliste neben den Rechner legen und sich nun voll den eigentlichen Problemen widmen kann, ohne zwischendurch überflüssige Dinge machen zu müssen.

Das Programm “Bezug" verarbeitet beliebige, korrekte ST-Pascal-Quelltexte und liefert die zugehörige Bezugsliste. Nach Angabe der Quelldatei und der Ergebnisdatei wird der gesamte Quelltext einer lexikalischen Analyse unterzogen und jedesmal, wenn das Token PROCEDURE oder FUNCTION auftaucht, wird der nachfolgende Prozedurname in das Array “Namen" eingetragen. Es reicht hier nicht aus, den folgenden Programmtext bis zum nächsten Prozedur/Funktionsaufruf einfach zu überlesen, weil so keine Unterprozeduren der aktuellen Prozedur erkannt werden können und falsche Zuordnungen entstehen würden. Entweder wird PROCEDURE oder FUNCTION erkannt und der Parser ruft sich rekursiv auf, oder es folgt mit BEGIN der Prozedurkörper. Hier muß nun die Anzahl aller BEGINs, CASEs und ENDs verrechnet werden, um das korrekte Prozedurende zu erkennen; auch hier reicht es nicht, den Programmtext bis zur nächsten Prozedur/Funktionsdeklaration zu überlesen. Alle Worte innerhalb des Prozedurkörpers werden nun mit den Einträgen in dem Array “Namen" verglichen. Wenn eine Übereinstimmung festgestellt wird, handelt es sich um einen Prozeduraufruf, der nun eingetragen werden darf:

  1. die aktuelle Prozedur A ruft eine Prozedur B auf.
  2. Die Prozedur B wird von der aktuellen Prozedur A aufgerufen.

Entweder wird ein neuer Eintrag vorgenommen oder ein Zähler wird weitergesetzt, wenn der Eintrag schon existierte (Mehrfachaufruf). Auf diese Weise werden alle aufgerufenen Prozeduren eingetragen und gleichzeitig kann bei den aufgerufenen Prozeduren vermerkt werden, von welcher Prozedur sie wie oft aufgerufen werden.

Hier muß ich kurz auf einen Sonderfall eingehen: Wenn eine Funktion betrachtet wird, entspricht die Wertzuweisung an den Funktionsnamen einer ganz normalen Zuweisung und darf daher nicht eingetragen werden. Beispiel aus der Fakultätsberechnung :

Function FAK ( n : Integer) : Integer;
...
FAK := FAK ( n-1 ) * n

Erkannt wird dieser Fall an folgenden drei Eigenschaften:

  1. der Testname entspricht dem Namen der gerade betrachteten Funktion (FAK= FAK), normalerweise ein rekursiver Aufruf.

  2. nach dem Testnamen steht eine Zuweisung.

  3. es handelt sich wirklich um eine Funktion (Funktion FAK). Im Beispiel wird also das erste FAK überlesen, weil es sich um eine Zuweisung an den Funktionsnamen handelt und nur das zweite FAK wird eingetragen, weil es sich hier um einen normalen rekursiven Aufruf handelt.

Ein weiterer Sonderfall ist die Behandlung von FORWARD-Deklarationen. Nachdem PROCEDURE/FUNCTION samt dem zugehörigen Namen erkannt wurden, werden nun eigentlich des BEGIN des Prozedurkörpers oder Unterprozedur-Deklarationen erwartet. Nach FORWARD kommt aber gar nichts von alledem, so daß hier auch nichts getan wird, außer daß der Name schon einmal eingetragen wird, damit er zum Erstellen der Bezugsliste verwendet werden kann. Beim zweiten Auftreten der FORWARD-Prozedur ist der Name schon bekannt und wird nicht noch einmal eingetragen, aber jetzt wird der vermißte Prozedurkörper endlich bearbeitet.

Wenn alle Bezüge erstellt sind, kann das Array nun sortiert oder in Reihenfolge des Eintrages ausgegeben werden. Als Sonderleistung kann man nun auch alle Prozeduren erkennen, die, aus welchem Grund auch immer, nie aufgerufen werden und deshalb keinen Eintrag besitzen. Mein persönlicher Rekord steht bei einem Programmsystem mit ca. 500 Einzelprozeduren, wo es wirklich unmöglich wird, einzelne Bezüge von Hand zu verfolgen. Das Programm hat mir hierbei sehr gute Dienste geleistet. Die Bezugsliste erleichtert einem die Wiedereinarbeitung in selbstgeschriebene Programme sehr, weil die Zusammenhänge einzelner Prozeduren schneller wieder klar werden. Die beste Lösung ist natürlich, die Bezüge gleich in der Prozedurdokumentation festzuhalten.

Das Programm kann Include-Dateien in dieser Version leider nicht selbständig bearbeiten. man muß also alle Include-Anweisungen durch den entsprechenden Programmtext ersetzen, was mir selbst allerdings als nicht allzu tragisch erscheint, denn es ist so immer noch wesentlich bequemer, als alle Include-Dateien nach bestimmten Prozedurnamen durchsuchen zu müssen.

{ (C) MAXON Computer GmbH, 1989 }

PROGRAM Bezug;

CONST {$I d:GEMCONST.PAS}
        max_etr = 600; { Array-Größe für die Prozedureinträge }

        StringLen =30; { Länge aller verwendeten Zeichenketten }

TYPE {$I d:GEMTYPE.PAS}
    Wortstring = string[StringLen]; 
    bezeichner = (iPROC,iFUNC,iBEGIN,iCASE,iEND,iFORWARD,iNIX);
    P_Uprogr   = ^Uprogr;
    Uprogr     = RECORD Nr, Anzahl : Integer ; next : P_Uprogr end;
    INFO       = RECORD
                    name : Wortstring;
                    PTyp : bezeichner; 
                    WIRD_AUFG_VON , RUFT_AUF : P_Uprogr; 
                 end;

VAR sortieren, ok : Boolean;
              ch : Char; 
        aus, dat : text; 
    inputname, outputname,path : string; { keine festgelegte Länge } 
               t : Wortstring;
             bez : bezeichner;
       ETR_Nr,i,
      Bez_Anzahl : Integer;
           Namen : array [0..max_etr] of INFO;
       Bezstring : array [bezeichner] of Wortstring;
         Zeichen : Set of Char;

($I d:GEMSUBS.PAS}

Procedure Kommentar;
{Prozedur Kommentar überliest so lange Zeichen, } 
{bis ein Kommentar-Ende-Zeichen gefunden wird.  }

begin
repeat
    read (dat,ch); 
    if ch='*' 
    then begin
        read (dat,ch);
        if ch=')' then ch := '}';
        end
until (ch = '}') or EOF (dat);
read (dat,ch);
end;



Procedure Readword ( VAR w : Wortstring; VAR zuweisung : Boolean );
{ Prozedur Readword sucht ab der aktuellen Position nach }
{ einer verwertbaren Zeichen/Ziffernfolge, die einen     }
{ Prozedur- oder Funktionsnamen darstellen kann und gibt }
{ dieses Wort nach Umwandlung in Großbuchstaben in der   }
{ Variablen w zurück. }
{ Beim Eintritt in diese Prozedur ist ch schon besetzt!  }
{ Wenn vor der nächsten Zeichenkette eine Zuweisung      }
{ steht, dann wird das an die aufrufende Proz. gemeldet. }

var i : Integer; 
begin
zuweisung := FALSE;
while not EOF (dat) and not (ch in Zeichen) do 
    if ch in [ ':', '{' , '(', #39 ] 
    then case ch of
        '{' : Kommentar;
        '(' : begin
              read (dat,ch); 
              if ch = '*' then Kommentar
              end;
        #39 : begin
              repeat read (dat,ch); until (ch = #39) or EOF (dat); 
              read (dat,ch); 
              end;
        ':' : begin
              read (dat,ch); if ch= '=' then zuweisung := TRUE; 
              end
        end { ch steht auf nächstem Zeichen } 

    else read (dat,ch);

{ ch steht nun auf gültigem Zeichen : )
i:=1; w:='';
while not EOF (dat) and (ch in Zeichen) and (i<=StringLen) do
    begin
    if ( ch >= 'a' ) and ( ch <= ’z' ) then ch := chr ( ord (ch)-32 ); {in Großbuchstaben umwandeln }
    w:= concat (w,ch);
    i:= i+1;
    read (dat,ch);
    end;

{ wirf den Rest des Wortes weg: }
while not EOF(dat) and ( ch in Zeichen ) do read (dat,ch);
end;

Function Suche_Bez ( wort : Wortstring ) : bezeichner;
{ testet, ob das Wort w eines der reservierten Worte ist  }
{ und liefert den gefundenen Bezeichner oder iNix zurück. }

var     x : bezeichner;
    found : Boolean;
begin
if length (wort) < 3 
then Suche_Bez := iNIX 
else begin
     x:= iPROC; found := FALSE; 
     repeat
        if wort = Bezstring[x] 
        then found := TRUE 
        else x:= succ (x); 
     until (x=iNIX) or found;
     Suche_Bez := x; 
     end;
end;


Procedure Beziehung_eintragen ( Bez1, Bez2: Integer );
{ Trägt die neuen Beziehungen zwischen den Indizes Bezl und Bez2 ein. }
{ Es wird eingetragen: a) Bez1 ruft Bez2 auf,               }
{                      b) Bez2 wird von Bez1 aufgerufen.    }
{ Wenn ein Eintrag schon existiert, dann ist Bez2 schon einmal von Bez1 )
{ aufgerufen worden und dann wird nur der Aufruf-Zähler erhöht. }

VAR       p,q : P_Uprogr;
    vorhanden : Boolean;

    Procedure Such ( Suchnr : Integer );
    Label 99;
    Begin
    vorhanden := FALSE; 
    q:=p;
    while (p <> NIL) do 
        begin
        if p^.Nr = Suchnr
        then begin          { schon eingetragen, Zähler erhöhen und }
            p^.Anzahl := p^.Anzahl +1; { das Vorhandensein melden. } 
            vorhanden := TRUE; 
            goto 99; 
            end;
        q:=p;
        p:=p^.next; 
        end;
    { q zeigt hier auf das letzte Element der Kette oder auf NIL, }
    { wenn noch kein Eintrag in der Liste existierte } 
    new (p); 
    p^.next := NIL; 
    p^.Anzahl := 1;
    99:
    end;

begin
Bez_Anzahl := Bez_Anzahl + 1;   { alle Aufrufe mitzählen } 
p := Namen [Bez1].RUFT_AUF;
Such (Bez2); 
if not vorhanden
then begin      { Bez2 ist in der Ruft_auf-Liste noch nicht eingetragen } 
    p^.Nr := Bez2; 
    if q=NIL
    then Namen [Bez1].RUFT_AUF := p {Erst-Eintrag in der Liste } 
    else q^.next := p; { p hinter den letzten Eintrag hängen }
    end;
p:= Namen [Bez2].WIRD_AUFG_VON;
Such (Bez1); 
if not vorhanden 
then begin
    p^.Nr := Bez1; 
    if q=NIL
    then Namen [Bez2].WIRD_AUFG_VON := p
    else q^.next := p;
    end;
end;

Function Namen_suchen ( w: Wortstring ): Integer; 
{ Durchsucht das Array nach dem Namen w und liefert den }
{ Array-Index des Eintrages oder 0 zurück }

var     i : Integer;
    found : Boolean; 
begin
found := FALSE; 
i := 1;
while (i<=ETR_Nr) and not found do 
    if w = Namen [i].name 
    then found := TRUE 
    else i:= i + 1; 
if found
then Namen_suchen := i 
else Namen_suchen := 0; 
end;

Procedure Namen_eintragen ( Name: Wortstring;
                            PTyp: bezeichner );
{ Belegt den nächsten freien Platz im Array mit } 
{ einem neuen Namen und zugehörigen Typ.        }

begin
ETR_Nr := ETR_Nr + 1; 
if ETR_Nr <= max_etr 
then begin
    Namen [ETR_Nr].name := Name;
    Namen [ETR_Nr].PTyp := PTyp; 
    end
end;


Procedure Parser (aktNr, level : Integer);
{ Folgt der Struktur des vorliegenden Quelltextes und trägt die gefundenen }
{ Beziehungen zwischen den Prozeduren ein.)
{ aktNr der Index der Prozedur, in deren Körper man sich gerade}
{ befindet.}
{ level : Ebene der Unterprozedur; das Hauptprogramm liegt auf level=0, }
{ alle dort deklarierten Prozeduren auf level=l und deren Unter-}
{ Prozeduren liegen auf noch höheren Ebenen; diese Information }
{ könnte auch noch ausgewertet werden.}

VAR w,w2, Ptyp : Wortstring; 
    pos, Schachtelungstiefe : Integer;
    zuweisung : Boolean;
LABEL 9, 99;

begin
9:
repeat
    Readword (w,Zuweisung); 
    bez := Suche_Bez (w); 
until (bez in [iPROC, iFUNC, iFORWARD, iBEGIN ]) or EOF (dat); 
if EOF (dat) then goto 99;

case bez of
    iPROC, iFUNC: 
        begin
        Ptyp := w;  { w enthält hier PROCEDURE oder FUNCTION }
        Readword (w,zuweisung); {nun den Prozedurnamen lesen }
        pos := Namen_suchen (w); 
        if pos = 0 
        then begin
            Namen_eintragen (w, bez );
            Parser ( ETR_Nr, level+1 );
            end
        else Parser ( pos, level+1 );   {Name schon bekannt, weil er }
{mit einer forward-Deklaration eingetragen wurde.} 
{Der im Text folgende Prozedurkörper wird im nächsten Level }
{bearbeitet, wo auch weitere Unterprozeduren erkannt werden.}
        goto 9; { alle weitere Prozeduren / Funktionen abarbeiten, }
{die auf der gleichen Deklarationsstufe stehen.} 
        end;
    iBEGIN : { dies ist jeweils erstes BEGIN einer Prozedur } 
        begin { oder auch des Hauptprogrammes. }
        if level = 0
        then aktNr := 0; {Hauptprogramm erkannt.} 
        Readword ( w,zuweisung );
        Schachtelungstiefe := 1; { 1, weil gerade BEGIN erkannt wurde. }
        { BEGIN/ END muß mitgezählt werden, um das Prozedurende zu erkennen } 
        while not EOF (dat) do 
            begin
            bez := Suche_Bez (w); 
            if bez in [iBEGIN, iCASE] 
            then Schachtelungstiefe:=Schachtelungstiefe+1; 
            if bez = iEND 
            then begin
                Schachtelungstiefe:=Schachtelungstiefe-1; 
                if Schachtelungstiefe = 0 { Rekursionsstufe verlassen, } 
                then goto 99; { wenn ein Prozedurkörper abgearbeitet wurde.} 
                end;
            { Um eine FunktionswertZuweisung zu erkennen, wird hier schon }
            { das nächste Wort eingelesen, aber noch zurückgestellt:      }
            Readword ( w2, zuweisung ); 
            pos := Namen_suchen (w); 
            if (pos <> 0)
            then if ( aktNr = pos ) and zuweisung and
                    ( NAMEN [pos].PTyp = iFUNC ) 
                 then { Funktionsergebnis, nicht mitzählen } 
                 else Beziehung_eintragen ( aktNr, pos );
            w : = w2; 
            end;
        end;
    iFORWARD : { keine Bearbeitung, weil hier kein Prozedurkörper folgt }; 
    end; { of case }
99:
end;

Procedure Ausgabe;
{ Erzeugte Bezugsliste sortiert oder in Reihenfolge des Eintrages ausgeben. }

type SortListptr = ^SortList;
     SortList = RECORD
                    item : Integer; 
                    next : SortListptr; 
                end;
var p : P_Uprogr; 
    i : Integer;
    SL, sp,sx, slast : SortListptr; 
    found : Boolean;

        Procedure Listenausgabe ( p : P_Uprogr );
        { gibt alle Prozedurnamen aus, die in der Liste p vermerkt sind } 
        begin
        if p=NIL then writeln (aus,'----')
                 else writeln (aus); 
        while p <> NIL do 
            begin
            writeln (aus,' ',p^.Anzahl:3,'x',Namen[p^.Nr].name:StringLen); 
            p:= p^.next 
            end; 
        writeln (aus); 
        end;

        Procedure Eintragausgabe ( i: Integer );
        { gibt einen vollständigen Eintrag mit Namen und beiden Listen aus } 
        begin
        writeln(aus); 
        writeln(aus);
        writeln (aus,'$$ —-> ',bezstring [ NAMEN[i].PTyp ],
                     ' ',Namen [i].name,':');
        write (aus,'ruft auf:');
        Listenausgabe ( Namen [i].RUFT_AUF ); 
        write (aus,'wird aufgerufen von:'); 
        Listenausgabe ( Namen [i].WIRD_AUFG_VON ); 
        end;

begin
if sortieren 
then begin
    { lokale Liste aufbauen, in der die Array-Indizes der Namen }
    { nach diesen Namen aufsteigend sortiert eingetragen werden: }
    new ( SL );
    SL^.next := nil; SL^.item := 0;

    for i := 1 to ETR_Nr do 
        begin
        t := Namen [i].name; 
        sp := SL; 
        slast := sp; 
        found := FALSE;
        while ( sp <> nil ) and not found do
            if ( t > Namen[sp^.item].name ) 
            then begin
                slast := sp; 
                sp := sp^.next; 
                end
            else found := TRUE;

        new ( sx );
        if sp=SL
        then SL := sx
        else slast^.next := sx;
        sx^.next := sp; sx^.item := i;
        end;

    { SL zeigt auf den Anfang der sortierten Liste: }
    sp := SL;
    while sp <> nil do 
        begin
        Eintragausgabe (sp^.item);
        sp := sp^.next;
        end;
    end
else for i := 0 to ETR_Nr do { unsortierte Ausgabe }
        Eintragausgabe (i);
end;

Function Files: Boolean;
{ Ein / Ausgabedateien definieren und zurückmelden, }
{ ob gültige Dateinamen angegeben wurden.           }
{ bei Ausgabe = CON: geht die Ausgabe auf den Bildschirm }

begin
OK := Get_in_File ( path, inputname );
if OK then OK := Get_out_File(' Ausgabe nach:',outputname );
if OK
then begin
    RESET ( dat, inputname );
    REWRITE ( aus, outputname ); 
    end;
Files := OK; 
end;


begin { Hauptprogramm }
ETR_Nr :=0; Bez_Anzahl :=0;
Bezstring [iPROC] := 'PROCEDURE';
Bezstring [iFUNC] := 'FUNCTION';
Bezstring [iBEGIN]:= 'BEGIN';
Bezstring [iCASE] := 'CASE';
Bezstring [iEND]  := 'END';
Bezstring [iFORWARD] := 'FORWARD';
Bezstring [iNIX]  := '';
Zeichen := ['0'..'9','A'..'Z','a'..'z','_'];
Path := '*.PAS';    { z.B.: 'A:\*.PAS', Pfadvorgabe für Dateiauswahl }

if (Init_Gem >= 0) and Files 
then begin
    i := Do_Alert ('[2][ Ausgabe sortieren ? ][ Ja | Nein ]',1); 
    sortieren := ( i=1 );
    for i := 0 to max_etr do 
        begin
        Namen [i].name := '';
        Namen [i].PTyp := iNIX;
        Namen [i].RUFT_AUF := NIL;
        Namen [i].WIRD_AUFG_VON :=NIL; 
        end;
    Namen [0].name := 'HAUPTPROGRAMM'; 
    while not EOF (dat) do 
        Parser(0,0);
    CLOSE (dat);
    Ausgabe;
    writeln ( aus );
    writeln ( aus, '=== STATISTIK === '); 
    writeln ( aus, 'Anzahl der Prozeduren und Funktionen: ',Etr_Nr+1 );
    writeln ( aus, 'Anzahl der Beziehungen insgesamt:'); 
    writeln ( aus, Bez_Anzahl, ' RUFT_AUF - Einträge und'); 
    writeln ( aus, Bez_Anzahl, ' WIRD_AUFGERUFEN_VON - Einträge'); 
    CLOSE (aus);
    Exit_Gem;
    end;
end.


Alexander Mauksch
Aus: ST-Computer 09 / 1989, Seite 89

Links

Copyright-Bestimmungen: siehe Über diese Seite