← ST-Computer 09 / 1989

Prozedur Beziehungen

Programmierpraxis

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