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:
- die aktuelle Prozedur A ruft eine Prozedur B auf.
- 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:
-
der Testname entspricht dem Namen der gerade betrachteten Funktion (FAK= FAK), normalerweise ein rekursiver Aufruf.
-
nach dem Testnamen steht eine Zuweisung.
-
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.