Development system with methods for assisting a user with inputting source code6314559Abstract A visual development system having an interface which assists a user with input of source code expressions and statements during creation of a computer program is described. The interface includes an Integrated Development Environment (IDE) interface having a code editor with "Code Completion" and "Code Parameter" features for displaying context sensitive pop-up windows within a source code file. Code Completion is implemented at the user interface level by displaying a Code Completion dialog box after the user enters a record or class name followed by a period. For a class, the dialog lists the properties, methods and events appropriate to the class. For a record or structure, the dialog lists the data members of the record. To complete entry of the expression, the user need only select an item from the dialog list, whereupon the system automatically enters the selected item in the code. Code completion also operates during input of assignment statements. When the user enters an assignment statement for a variable and presses a hot key (e.g., <ctrl><space_bar>), a list of arguments valid for the variable is displayed. Here, the user can simply select an argument to be entered in the code. Similarly, the user can bring up a list of arguments when typing a procedure, function, or method call and needs to add an argument. In this manner, the user can view the required arguments for a method as he or she enters a method, function, or procedure call. Claims What is claimed is: Description COPYRIGHT NOTICE
Feature Use and functionality
Code Completion Enter a class name followed by a period in a code file.
The list of properties, methods and events appropriate
to the class will be displayed. The user can then select
the item to be entered in the code. Enter an assignment
statement and press <ctrl> <spacebar>. A list
of
arguments that are valid for the variable is displayed.
Select an argument to be entered in the code.
Code Parameters View the syntax of a method as the user enters it into
the code.
Tooltip When the compiler is stopped during debug, the user
Expression can view the value of a variable by pointing to it with
Evaluation the cursor.
Code Completion Set the duration of the pause before a Code Insight
Delay dialog box is displayed.
Code Templates Available templates are listed by name with a short
description. Click a template name to display the code
that will be entered in the file when that template is
selected. Code displayed in the code window can be
edited.
Templates The Templates box includes a name and short
description for each template.
Code The code box displays the code that will be inserted
into a file when the template is selected. The code
displayed can be edited.
Of particular interest herein are the Code Completion and Code Parameter features. Code Completion is implemented at the user interface level by displaying a Code Completion dialog box after the user enters a record or class name followed by a period. For a class, the dialog lists the properties, methods and events appropriate to the class. For a record or structure, the dialog lists the data members of the record. As shown in FIG. 5A, for instance, at 501, the user has begun entry of MainForm, a class of type TMainForm. Upon the user's input of the dot operator, the system automatically displays list dialog 503 next to the current cursor position. Dialog 503 lists the properties, methods and events appropriate to the class. To complete entry of the expression, the user need only select an item from the dialog list, whereupon the system automatically enters the selected item in the code. In a like manner, FIG. 5B illustrates Code Completion for a rectangle structure, TRect. Here, the user has declared a variable, myRect, of type TRect. The TRect type defines a rectangle as follows. TRect=record case Integer of 0: (Left, Top, Right, Bottom: Integer); 1: (TopLeft, BottomRight: TPoint); end; where TPoint is itself a record defined as follows. TPoint=record X: Longint; Y: Longint; end; In FIG. 5B, upon the user's input of the dot operator, the system automatically displays list dialog 513 next to the current cursor position 515. Dialog 513 lists the data members appropriate to the record (structure). Again, the user need only select an item from the dialog list to complete entry of the expression, whereupon the system automatically enters the selected item in the code. Code Completion also operates during input of assignment statements. As illustrated in FIG. 5C, when the user enters an assignment statement for integer variable J and presses a hot key (e.g., <ctrl><space_bar>) at 525, a list of arguments 523 valid for the variable is displayed. Here, the user can simply select an argument to be entered in the code. As shown in FIGS. 5D-E, the user can select a type which itself is not appropriate (e.g., record type) but nevertheless includes a nested data member having a type which is valid. In FIG. 5D, the user selects type variable SMTP1, a structure of type TSMTP, at 531. Upon the user entering the dot operator after SMTP1, the system displays a list of the data members for SMTP1, as shown at 541 in FIG. 5E. Now, the user can simply select a valid argument to be entered in the code. In a like manner, the user can bring up a list of arguments when typing a procedure, function, or method call and needs to add an argument. In FIG. 5F, for instance, the user has begun entry of a SendFile method call. The SendFile method itself is defined elsewhere in the code as follows. procedure SendFile(Filename: string); Upon the user entering the opening parenthesis, the system automatically displays parameter information for the call. The syntax for the argument(s) to the method is displayed, as shown at 551 in FIG. 5F. In this manner, the user can view the required arguments for a method as he or she enters a method, function, or procedure call. B. Overview of internal operation During basic operation, the Integrated Development Environment or IDE invokes the compiler for determining an appropriate context for the source code, based on where the screen cursor is currently positioned within the code. The compiler, in response, compiles the source code up to the current point (of the user's cursor) and then returns a result back to the IDE which describes the current context within the source code. The IDE receives two core pieces of information. If the user has positioned the cursor within the parameter list of a function call, the IDE will receive information from the compiler reporting the name of the function together with the name and the types of the function's formal parameters. With this information, the IDE can display a pop-up menu providing an argument list for the current function, thus eliminating the need to invoke a help system for looking up the function. The second type of information which the IDE receives relates to symbols or identifiers. If a symbol or identifier would be valid at the current cursor position, the compiler will identify the condition and prepare a list of the valid identifier, together with other valid identifiers, for the cursor position. The IDE, upon receiving this information, can display the list to the user. The user, in turn, selects the desired symbol from the list, for instance using incremental searching technique. As an example of this feature, consider for instance a variable name followed by the dot operator (e.g., MyRecord.). Here, a member name is expected (e.g., MyRecord.Foo). Accordingly, the compiler will compute and report the members of the structure (of the variable) which meet any conditions or constraints of the code at the cursor position. If an integer type is expected, for instance, the compiler here would only report integer members. The determination of an appropriate type is based on legal constructs which can be created at the then-current cursor position. In an expression comprising a floating-point assignment, both floating-point and integer data types are valid. Accordingly, both floating-point and integer data members would be displayed for user selection. The approach also takes into account nested members. Continuing with the example of an integer data type, one of the data members might be a rectangle structure (e.g., a structure of type RECT) which, in turn, comprises integer data members (i.e., nested data members). In this situation, the system displays the rectangle (nested structure) member since there is a legal way to employ integer data members of that structure. For this example, the user will ultimately need to type an additional dot (i.e., dot operator) to complete selection of the final data member. C. Core internal methods The core functionality is provided as follows. A first method, CompilerKibitz, is invoked by the IDE to trigger compilation. In invoking the method, the IDE passes in the filename (i.e., source code filename), together with the current line number and column position where the cursor is located. In an exemplary embodiment, the method may be constructed as follows (using the C programming language).
void EXPORT CompilerKibitz(CompOptions *options,
UnitNameFileNamePair *unitFileNamePairs,
const char *stopSrc, int stopLine, int stopCol,
char makeFlag, KibitzResult *result)
{
/ / . . .
/* Parse source up to stopSrc, stopLine and stopCol and report the
result. source and stopSrc will be the same except for
include files. */
/*
Two main pieces of information:
- Are we currently in a call to a user-defined or standard procedure,
function, or method? If so, what's the name of the procedure and the
parameter list. What are the positions of the actual parameters so far?
(This information is recorded directly in the result record in part;
however, the IDE has to call back for more information.)
- If a symbol was expected at the cursor position, what is the list of
symbols that would make sense at this point? For this
information, a GetValidSymbols method or function is invoked
*/
/ / . . .
}
As shown, the method is invoked with seven parameters. The first parameter, a pointer-to compiler options (CompOptions), is a structure which specifies a "source to compile"; this might be different than the source which the IDE desires the compiler to stop. The file can be specified in a conventional manner, such as by filename (text string). The second parameter is a pointer-to unit/file name pairs. This, in essence, is a table of unit name/file name associations. This is passed to the compiler since the IDE might have some knowledge about where particular files are located (on disk) for corresponding "units" which are included in the user's project. In a Borland Delphi.TM. program, for instance, a "uses" statement sets forth particular Pascal "units" which are employed. Since the IDE stores directory information for various files (e.g., library files), the IDE can pass such information on to the compiler via the second parameter. The next three parameters specify the user's source position. The stopSrc parameter is a (pointer to) character string specifying the "stop source"--that is, the current source file where the user has stopped (i.e., stopped data entry long enough to invoke the code completion methodology of the present invention). The stopLine parameter is an integer data member storing the particular line number where the user stopped in the source code. Similarly, stopCol is an integer data member specifying the particular column in the source code where the user stopped. Finally, the makeFlag parameter serves as a flag indicating that the compiler should "make" the program (i.e., compiling all dependent source files for ensuring that the currently-compiled unit or project is up-to-date). This is set to "true" when the IDE has detected that other files have been added, thus requiring a "make." If, on the other hand, other files have not been added, the flag is set to false and only the current source file is compiled. The last parameter is a pointer to a "Kibitz" result structure, KibitzResult. After completion of the method execution, the result structure still does not contain a list of valid symbols. Instead, the IDE invokes yet another method to fill out this particular information in the result structure. To obtain the actual list of valid symbols, the IDE invokes a GetValidSymbols method. The method includes the following prototype.
int EXPORT GetValidSymbols(const KibitzResult *k, Symbol **result,
GF_Flags *flags, int maxCnt)
{
/* Find symbols valid in this context. Find up to maxCnt, and report
how many were found. Write them into result, if result <> 0.
*/
}
As shown, the method is invoked by passing in (by reference) the KibitzResult structure, as the method's first parameter. Together with the result structure, a vector of symbols is passed in as the second parameter and a vector of flags is passed in as the third parameter. Finally, a maximum count is specified in the fourth parameter, for indicating an upper limit on the number of symbols which should be returned (i.e., based on how many the IDE can realistically handle). During system operation, the IDE invokes the GetValidSymbols method twice. On the first invocation of the method, the IDE simply specifies NULL for the two vectors whereupon the method returns a count for the number of valid symbols. Based on this first call, the IDE will allocate sufficient memory and then invoke the GetValidSymbols method a second time, passing in appropriate pointers to the allocated vectors which are to store the symbol results. D. KibitzResult data structure The result data structure, KibitzResult, may be defined as follows.
typedef struct
{
KibitzKind kind;
Unit *unit;
int scopeCnt;
Symbol *scopeList [MAXSCOPES/*scopeCnt*/];
Symbol *proc; // valid for KK_ARGUMENT and KK_STD_ARG
Symbol *formalArg; // valid for KK_ARGUMENT
int formalInx; // valid for KK_STD_ARG, 1-based
unsigned validTypes; // valid for KK_TYPE, KK_STD_ARG
Type *validType; // valid for KK_EXPR, KK_CONST_EXPR
Token validToken; // valid for KK_TOKEN
TokenClass validTokens; // valid for KK_TOKENS
Kibitzpos pos; // position of kibitz point
char partialInden[64]; // may have found a partial identifier
// KK ARGUMENT: where the parms were; [0] <=> `(` .vertline. `[`
pos
LineCol paramPos [MAXPARAMS + 1];
} KibitzResult;
As shown, the data structure is a record storing context information. Of the various data members of the record, the most important is KibitzKind, an numerated type indicating the "kind" of source code situations the system is currently in. The KibitzKind data type is itself defined as follows.
typedef enum
{
KK_NONE, // Not something we support
KK_FIELD, // A field of a record/object/class
KK_ARGUMENT, // An actual argument to a user-declared
// function or procedure
KK_STD_ARC, // An actual argument to a standard
.sup. function/procedure
KK_TYPE, // A type
KK_EXPR, // An expression
KK_STMT, // A stmt
KK_CONST_EXPR, // A constant expression
KK_TOKEN, // A token
KK_TOKEN_CLASS, // A set of tokens
KK_PROC_DECL, // A procedure, function, method,
.sup. constructor,
// or destructor declaration
KK_ERROR, // Error case where we don't have
.sup. enough info
KibitzKinds,
} KibitzKind;
Two simple types are KK_NONE and KK_ERROR. KK_NONE indicates that the cursor is currently positioned at a location where code completion is not supported, such as positions within a user's comment. KK_ERROR, on the other hand, indicates an error condition; this occurs when the user's program contains so many errors that the system cannot correctly determine appropriate code completion. The KK_FIELD data type indicates a field of a record (structure), object, or class. For instance, if the user types Form1, a field member of Form1 is expected. KK_ARGUMENT indicates that the system expects an actual argument to a user-declared function or procedure. Here, further restrictions can be imposed. If the argument is a var argument, then the argument must be a variable. If, on the other hand, the argument is a value, then an expression is acceptable. KK_STD_ARG indicates that an actual argument to a compiler-defined, standard function/procedure is expected. Since such functions or procedures have special requirements, the system treats them separately. KK_TYPE indicates that the system expects a "type," such as within a declaration statement. KK_EXPR indicates that an expression is expected. KK_STMT indicates that a statement is expected--that is, the system expects any valid identifier which can start a statement. Here, both a variable or procedure would be acceptable, for instance. KK_CONST_EXPR indicates that a constant expression is expected. This is typically employed for declarations, such as for string declarations. A type expression can be applied here, such as for indicating that an integer expression is required. KK_TOKEN and KK_TOKEN_CLASS indicate that the compiler is expecting a token or a particular type or class of token, respectively. In Borland Delphi.TM. (using Object Pascal), for instance, when an "if" token followed by a boolean expression is encountered, the compiler then expects a "then" token. KK_PROC_DECL indicates a procedure (function, method, or the like) declaration. In such a case, the system can display a list of all such procedures, functions, or methods which were forward declared. Returning to the description of the KibitzResult structure, the second data member is unit. The unit data member points to a data structure (internal to the compiler) which represents the unit, as compiled by the compiler. This is followed by a scope count, scopeCnt, and a scope list, scopeList, for representing the available scopes at this particular point in the source code. For instance, when the user starts a statement within a procedure, all of the local symbols of the procedure represent one scope. If the procedure is in fact a class method, then all of the fields of "self" (i.e., of the "self" hidden parameter) would represent another scope. The symbols of the unit would represent still yet another scope. Still further, there are symbols of other units which the current unit employs giving rise to yet another scope. The scope count and scope list keep track of which of these scopes are available at a given point in the source (where the cursor is positioned). This information is ultimately used by the GetValidSymbols method which "walks" the scope list for determining the valid symbols for the current source position under exam. The next data member, proc, is a pointer to a procedure symbol which is employed when the system is processing a call. The formalArg data member is a symbol pointer which points to the formal argument which the cursor is currently positioned at. For a function having three arguments, for example, the cursor could be positioned at the second argument. The next data member or field,formalInx, is an integer data member providing an index into the formal arguments. The validTypes data member is an internal encoding of the compiler which keeps track of which types will be valid at this point (in the source code). In the cases where the system expects an expression, such as an assignment into a variable, the pointer to validType references the valid type for the variable. The next two data members, validToken and validtokens, keep track of the expected tokens and token class. A token class represents a scenario where several tokens can be valid. The next data member, pos, represents the position where the user's cursor is positioned in the source code. The pos data member is implemented as a record or structure of type KibitzPos which may be implemented as follows.
typedef struct
{
short fileIndex;
short lineNo; // up to here idential to SourcePos
short columnNo; // 1-based
} KibitzPos;
As shown, the KibitzPos structure specifies a file (via an index), a line number, and a column number. The next data member of KibitzResult is partialIden, which is employed for instances where the system has found a partial identifier. Here, the IDE will employ the information to perform an incremental search of available identifiers for attempting to find a match. As its final data member, the KibitzResult structure includes a parameter position array, paramPos. This indicates the source position of the actual parameter for a function call. E. UnitNameFileNamePair data structure The UnitNameFileNamePair data structure may be constructed as follows.
typedef struct
{
uchar *unitName;
char *unitFileName;
} UnitNameFileNamePair;
This data structure is employed to keep track of unit names (e.g., simple Pascal names in Borland Delphi.TM.) and filenames (i.e., qualified by file directory and drive). The compiler employs this data structure for determining a filename (i.e., the name of a file on disk) which corresponds to a unit name encountered in the source code. The system defines the following flags which are used when a list of identifiers are reported back from the compiler to the IDE, for keeping track of whether a particular identifier itself is acceptable or whether the identifier itself only has a field which would be acceptable.
typedef enum
{
GF_NORMAL = 0.times.00, // Normal case
GF_SCOPE = 0.times.01, // Symbol was included because it
has a scope
GF_METATOKEN = 0.times.02, // Symbol is a metatoken
(e.g. `identifier`)
} GF_Flags;
This information is employed by the IDE to make a "visual" distinction between the two cases. Consider, for instance, an assignment statement involving an integer variable, as follows.
var
I, j : Integer;
r : TRect;
begin
I := // complete this statement
In the snippet above, I is an integer variable and, thus, the statement can be completed by assigning the integer j as follows. I:=j; Alternatively, the statement can be completed by assigning an integer member of r (which itself is a variable of type TRect). To indicate these two choices, the IDE displays a list showing j normally and showing r with an ellipsis appended to it. This informs the user that r is acceptable but that the user has to still complete the entry with a dot (for accessing a field of r). In this case, the GF_SCOPE would be set for r, indicating that it is acceptable but only because it has a field which is acceptable. F. Token-based processing The basic internal operation of the system is as follows. As an initial task, the system lexical analyzer has to take the source position that the IDE passed to it and transform it into a special token. In general compiler operation, the function of the lexical analyzer is to take source text and transform it into a stream of tokens. Consider the following code snippet. if I=.vertline.0 then (where .vertline. represents cursor position) In operation, the lexical analyzer converts the expression into a sequence of tokens, for instance as follows.
if I = .vertline. 0 then
(*
Tokens:
TK_IF
TK_IDENT
TK_EQUAL
TK_KIBITZ
*)
Here, "if" is represented by the TK_IF token, I is represented by the TK_IDENT (i.e., identifier) token, and so forth and so on. In accordance with the present invention, the scanner reports a special token, TK_KIBITZ, for marking the current cursor position. In this manner, the system is easily able to identify the cursor position in the stream of tokens. Exemplary method steps for performing this operation are as follows.
/*
Lexical analyser (scanner) delivers special token (TK_KIBITZ) at
cursor position. The following method checks against stop line once
per line, then sets special marker character in the line buffer.
*/
void Scan(void)
{
switch (GetNextInputChar())
{
case `.backslash.n`:
scannerState. lineNum++;
if ( scannerState.lineNum == stopline
&& strcmp(scannerState.fileName, stopSrc) == 0)
{
/* set special character to mark stop position */
lineBuffer[stopCol] = 0;
}
break;
case 0:
if (scannerState.column == stopCol)
{
token.tok = TK_KIBITZ; // insert special token
break;
}
/* . . . */
As shown, the implementation comprises a "switch" statement over the next input character (i.e., from the source code stream). Two special cases are of interest. The first case of interest is a line feed character. Upon reaching such an input character, the system at this case arm checks whether the current line number and filename correspond to that specified for the "stop" source (i.e., the source file where the user's cursor is positioned). As shown above, the line number is examined first so that the system can perform an integer comparison first, before a more expensive string comparison is undertaken. In the event that the line number and source filename indicate that the current input character corresponds to the cursor position, the system proceeds to mark the current line (maintained in a line buffer) with a special character, such as zero (i.e., NULL). After the line is marked, the system "switches" on subsequent input characters until it encounters the value of zero (i.e., the special character inserted above to mark the stop position). The system at this point confirms that this is the position of interest by comparing the current column (in the source stream) with the column from the IDE. If the current column is equal to the column where the system wishes to stop, then the system inserts the special token--TK_KIBITZ--that tells the parser to stop at this point. This method step of the scanner illustrates the transformation from a stop source, a stop line, and a stop column to a special token which instructs the parser where to stop. The parser in turn compiles the source normally, including the declaration, until it encounters the special token. During this compilation, however, the parser will skip compilation of function bodies unless a particular function body contains the special token. For code completion, in accordance with the present invention, the code contained within function bodies (apart from a current function which the user cursor might be positioned within) is not relevant. Since function bodies are skipped, no code is generated for those function bodies and the background compilation for code completion, therefore, occurs quickly. During processing, the parser maintains a stack of "kibitz" contexts. When the parser encounters a TK_KIBITZ token, it returns the context information back to the IDE. This is illustrated, for instance, by the following snippet.
variable := FooFunction( a, b, .vertline.
KK_EXPR, type of variable
KK_ARGUMENT, FooFunction
Argument 3, formal parameter
Note:
Parser keeps stack of kibitz contexts (on the runtime stack, for a
recursive decent parser), and returns the information back to the IDE when
it finds the TK.sub.13 KIBITZ token.
An exemplary method for skipping the body of functions, using the kibitz token, may be constructed as follows.
/*
Parser parses declarations fully, tries to skip function bodies,
suppresses code generation. Routine to skip keeps track of nesting of
structured statements, returns TRUE if the function body was
skipped, FALSE otherwise.
*/
static int SkipBody(void)
{
int nestLevel;
long startPos;
startPos = ScannerPos();
nestLevel = 0;
while (1)
{
switch (token.tok)
{
case TK_CASE:
case TK_TRY:
case TK_ASM:
case TK_INITIALIZATION:
case TK_BEGIN: nestLevel++; break;
case TK_END: nestLevel--; break;
case TK_EOF: nestLevel = -1; break;
case TK_KIBITZ:
ScannerSeek(startPos);
Scan();
return FALSE;
}
if (nestLevel < 0)
break;
Scan();
}
return TRUE;
}
The SkipBody method is invoked when the parser detects the beginning of a function body. The method operates as follows. At the outset, the method remembers the current position, as storing it to startPos. Next, the method initializes a local variable, nestLevel, which serves to keep track of the nesting of structured statements. This is followed by a "while" loop which switches on token. In operation, the loop reads a token and then switches to a particular case arm based on token type. If the token type marks the beginning of a structured statement, the nestLevel countered is incremented. Conversely, if this token marks the end of a structured statement, nestLevel is decremented. If the method encounters the special "kibitz" token, it returns to the beginning of the function by invoking ScannerSeek. Thereafter, the method returns "false" as the method has not been skipped. After completing execution of the switch statement, the method can determine whether it has found the final end of the function. In summary, the basic approach is to go through the function as fast as possible (i.e., look through the tokens of the function as fast as possible), keeping track of nesting. If the end is encountered, the method can return true. If, on the other hand, a special "kibitz" token is encountered, the method repositions the parser (scanner) to the beginning of the function and returns "false." In such a case, the parser will resume parsing forward as more detail about the function is required, since it is the function where the user's cursor is positioned. G. Scopes While parsing, the compiler keeps track of accessible scopes. For instance, when entering a particular statement, the parser enters a scope, and at the end of the statement the parser exits the scope. Scopes that are accessible include "with" scopes (i.e., Pascal with statements), local symbols of any current procedures the parser is within, the symbols of the current unit (i.e., symbols global to the unit), and symbols of any units employed by the current unit (i.e., imported symbols). In order to handle method function calls directly, the parser maintains a stack of contexts which it is in. Just as the computer system stack must maintain a separate copy of local variables for each function invocation, the parser mirrors the approach so that it can keep track of the appropriate context. During parsing of a function call, for instance, when the parser encounters the left paren (i.e., opening parenthesis), the parser "pushes" onto its stack information indicating it is now parsing within the context of a function argument. When the parser has parsed the right paren (i.e., closing parenthesis), the parser "pops" the previously-pushed stack entry, for indicating that it is no longer within the context of function argument. If the function call occurred in an assignment statement, for instance, the context information would indicate at this point that the parser is popping back into the context of an expression. In the instance of nested function calls (e.g., recursion), the stack would contain entries for different argument contexts. When in the context of an argument list for either a user-declared or standard function, the IDE employs ancillary or helper functions, to get information pertaining to the function and parameter symbols. Exemplary helper functions include the following (shown in function prototype form).
/* In the case of KK_ARGUMENT or KK_STD_ARG, we are in a
call to a user-declared or standard function, respectively. The IDE
uses the information in the result record and the following compiler
entry points to build a parameter list to show to the user:
*/
int EXPORT GetSymbols(Symbol *root, SQ_Flags flags,
Symbol **symList);
/* used to get the parameters of a procedure/function/method */
GSF_Flags EXPORT GetSymbolFlags (Symbol *sym);
/* used to get information about parameter and function symbols */
void EXPORT GetSymbolText(Symbol *sym, String text,
ST_Flags flags);
/* used to get names of symbols */
Symbol *EXPORT GetResultType(Symbol *sym);
/* used to get the result type of a function */
The GetSymbols method is employed to get the parameters (symbols) of a called procedure, function, or method. The root parameter is the routine being called; this is obtained from the result record. The SQ_Flags parameter is employed to indicate whether the GetSymbols routine should return parameter information. The symList parameter is a pointer to a result vector, which is employed for returning the parameters. The GetSymbolFlags method is employed for getting additional information about parameter and function symbols, such as whether a parameter is a var or const parameter. The routine can also be invoked on the procedure or function symbol itself for determining whether the symbol is a procedure, function, or method. The GetSymbolText method is employed to get the actual text for a symbol, such as the actual name of a parameter. The routine can also be used (with a particular flag setting) to get the name of the type of symbol. This information is used by the IDE to construct the parameter list for display to users. The GetResultType routine is employed to get the result type of a function (i.e., the symbol that represents the result type of a function). After obtaining this symbol, the IDE in turn can invoke GetSymbolText for getting the text or name for the symbol. H. Determining valid symbols As previously described, the GetValidSymbols method is employed to get a list of valid symbols. As previously described, the IDE will usually invoke the method twice; first, to get a count of valid symbols, and second to actually get the result (of symbols). In all cases except KK_NONE and KK_ERROR, the IDE can request a list of valid symbols via GetValidSymbols. As the IDE has no way of knowing how much space needs to be allocated for the result, the usual procedure is to call GetValidSymbols with a result and flags parameter of 0 in which case only the count of valid symbols will be reported. After allocating big enough buffers, the system can pass the information to a second call with otherwise identical parameters. Internally, GetValidSymbols typically invokes logic steps that traverse or walk all accessible scopes, applying to each symbol found in a "validation" function that checks whether the symbol would be accessible. Next, the routine invokes additional logic steps for examining each symbol for determining whether it is valid in the current context. If the symbol would be acceptable, it is reported to the IDE; if not, on the other hand, it is skipped. An exemplary validation function for the context KK_CONST_EXPR (i.e., constant expression), for instance, may be constructed as follows (simplified for clarity of description).
static int ValidConstExpr(const KibitzResult *k, Symbol *actualArg)
{
Type *argType;
if (actualArg->kind != SY_CONST)
return FALSE;
return IsAssCompat (k->validType, actualArg->type);
}
As shown, the function first checks whether the symbol is a constant; if not, the symbol is immediately rejected. If the symbol is a constant, the function then determines whether it is assignment compatible with the type which is expected. The function returns "true" if the symbol is assignment compatible; otherwise, the function returns "false." Routines are provided for walking different kinds of scopes. A routine that traverses the list of local symbols for a unit, for instance, may be embodied as follows.
static int WalkUnitLocals(Symbol *sym, const KibitzResult *k,
Symbol **result, GF_Flags *flags, int maxCnt, ValidSymbolProc *v)
{
Type *t;
int cnt;
cnt = 0;
for ( ; sym; sym = sym->next)
{
if (cnt >= maxCnt) // stop if we reached the maximum count
return cnt;
if (sym->name[0] < `A`) // skip invisible, compiler-generated
continue; // symbols
if (sym->decLevel != 0) // skip local procedures
continue;
if (v(k, sym, 0)) // is this symbol acceptable?
{
if (result)
result[cnt] = sym; // yes: report it back
if (flags)
flags[cnt] = GF_NORMAL;
cnt++;
else if (SymbolHasScope(sym, 0, k, v)) // does it have an
{ // acceptable field
if (result)
result[cnt] = sym; // yes: report it
if (flags)
flags[cnt] = GF_SCOPE; // and flag it as such
cnt++;
}
}
return cnt;
}
The method is invoked with the following parameters: a list of symbols (i.e., local symbols of the unit), a result record, a result vector, a flags parameter, a maximum count, and a pointer to a validation procedure. In operation, the method traverses the symbol list, keeping track of the count of acceptable symbols. At the "for" loop, the method determines whether it already has a sufficient number of symbols; if so, the method is done and may return. Otherwise, the method enters the "for" loop. Within the "for" loop, the method skips compiler-generated names; these will have names which are not legal for the underlying computer language (e.g., Pascal). Next, the method skips objects which are not at the global declaration level, such as local functions. If the symbol passes these tests, the method applies a validation function to it. If the validation function returns "true," then the symbol is flagged as a normal symbol. Even if the symbol is not accepted by the validation function, it might nevertheless have a field which is acceptable (e.g., a member field of a structure). If the symbol (or a field of a symbol) is acceptable, it is added to the result array; corresponding flags for the symbol are set in the flags array. As an optimization, the IDE performs a CompilerKibitz call in the background at certain times, so used units, source files, and the like get loaded into the compiler or the operating system's disk cache. I. Avoiding infinite cycles A particular difficulty is encountered when determining whether a structure has an acceptable field. If precautions are not taken, the system might be trapped in a "cycle." Consider, for instance, a TForm data structure. A TForm structure often includes components which are themselves of type TForm. Accordingly, extra care is required to avoid infinite incursion when traversing or walking a field list of a structure. To prevent this problem, the system adopts the following approach. For each type examined, the system keeps track of the "state" of the type--that is, what the system knows about the type. In an exemplary embodiment, four states are defined as follows.
// states a type can be in - all types start out as TS_UNKNOWN
typedef enum
{
TS_UNKNOWN, // don't know whether this type is acceptable
TS_FALSE, // know this type is not acceptable
TS_TRUE, // know this type is acceptable
TS_ACTIVE, // working to find out whether this type is
acceptable
} TypeState;
The first state, TS_UNKNOWN, indicates that the system does not know whether the type is acceptable. The second state, TS_FALSE, indicates that the system knows that the type is not acceptable. As an example of use, an assignment statement which assigns a floating point variable to an integer variable will not be acceptable and, thus, would be identified as TS_FALSE. TS_TRUE, the third state, indicates that the type is acceptable. Finally, TS_ACTIVE indicates a state where the system is working to determine whether the type is acceptable. A method, SymbolHasScope, for determining whether a symbol has an acceptable type for a given context may be constructed as follows.
// find out whether a symbol with acceptable type can be reached
// from type
static int SymbolHasScope(Type *type, const KibitzResult *k,
ValidSymbolProc *v)
{
Type *type;
switch (type->g.form)
case TF_CLASSREF: // only these types have scopes at all
case TF_INSTANCE:
case TF_RECORD:
case TE_OBJECT:
switch (GetTypeState (type))
{
case TS_TRUE: return TRUE; // know the answer is TRUE
case TS_FALSE: return FALSE; // know the answer is FALSE
case TS_ACTIVE: return FALSE; // cut off recursion here
case TS_UNKNOWN: // need to do some work here
SetTypeState(type, TS_ACTIVE);
if (WalkFieldScope(type, k, v)) // any acceptable fields?
{
SetTypeState(type, TS_TRUE); // yes: remember
return TRUE; answer is TRUE
}
else
{
SetTypeState(type, TS_FALSE); // no: remember
return FALSE; // anser is FALSE
}
}
default:
return FALSE; // type doesn't have scope
}
}
As shown, the method is implemented as a large case or "switch" statement which serves to determine whether the type has any scope at all (i.e., has fields). If it does have a scope, the method proceeds to retrieve the state of the type. This state is tested by a nested "switch" statement. If the state is known to be true (TS_TRUE) or false (TS_FALSE), the method can return "true" or "false," respectively. If, on the other hand, the state is "active" (TS_ACTIVE), the method returns "false," for presenting incursion or reentry. If the type is unknown, the method proceeds to determine the type as follows. First, if type state is set to "active" (TS_ACTIVE), for indicating that the system is in the process of determining the state. Next, the method traverses or walks the list of symbols for this scope, by invoking a WalkFieldScope helper routine. This call will determine whether the structure has a field which is accepted by the validation function. If it is acceptable, WalkFieldScope will return "true," whereupon the state of the type can be set to "true" (TS_TRUE). Otherwise, the state is set to "false" (TS_FALSE). Also, if the type does not have a scope, the SymbolHasScope method returns "false." As an optimization to avoid having to initialize all types to TS_UNKNOWN before calling the above routine, an additional table and an auxiliary field in the type structure is used to cache type results as follows.
type struct
{
TypeState typeState; // state of the type
Type *type; // pointer back to type
} TypeEntry;
static TypeEntry *typeTab;
static ulong typeCnt;
static ulong maxCnt;
Here, the system can index into the table for determining whether the type has already been processed before. While the invention is described in some detail with specific reference to a single-preferred embodiment and certain alternatives, there is no intent to limit the invention to that particular embodiment or those specific alternatives. Thus, the true scope of the present invention is not limited to any one of the foregoing exemplary embodiments but is instead defined by the appended claims.
|
Same subclass Same class Consider this |
||||||||||
