Ignoring user-defined types and data aggregates makes C symbol table management pretty easy. The complexity of the symbol table management still really depends on your task. If you simply want a pretty-printer or something you might just check to see that all variables are defined (maybe not even that). If you want to translate C to another language or machine code then you need type information. During translation you would need to ask "how do I translate int to my target language?".
Let's assume we need full type information on functions_and _variables. In terms of scopes, we only need to worry about the file scope, the parameter scope, and the local scope. Ultimately, we need to be able to answer the question: "is symbol x defined and, if so, what is its type?"
The basic idea will be to create a new Scope (list of symbols) when you enter a new scope even if the scope is nested such as when you enter a new left curly. Variable or function definitions are added to the symbol table associated with the current scope. References to symbols are resolved by looking for it starting in the current scope. If the current scope doesn't have it, you look in the enclosing parent scope and so on. If you get to the global scope, the symbol is undefined. For example, to find x, your translator would look in the IF scope, then the local scope, then the parameter scope, and finally the global scope before it found the definition:
int x = 3;
void foo(int i, int j) {
int y;
if ( i==0 ) {
int z;
z = x;
}
if (x>0) {
int q;
}
}
In the IF statement after the q definition, the stack of scopes would look like:

where the back pointers point to the enclosing scope.
This mechanism is easily embodied by the following Scope class:
class Scope {
Scope parent = null;
Map symbols = new HashMap();
public Scope(Scope parent) {
this.parent = parent;
}
public Scope pop() { return parent; }
public Symbol defineVariable(String name, Symbol type) {
Symbol s = new VariableSymbol(name,type);
symbols.put(name, s);
}
public Symbol defineFunction(String name, Symbol returnType) {
Symbol s = new FunctionSymbol(name,type);
symbols.put(name, s);
}
public Symbol lookup(String name) {
Symbol s = symbols.get(name);
if ( s!=null ) {
return s; }
if ( parent!=null ) {
return parent.lookup(name);
}
return null; }
}
Where Symbol is just an object that maps a variable to its type:
public class Symbol {
public String name;
public Symbol type;
/** point to def of symbol in tree */
AST def;
public Symbol(String name) {
this.name = name;
}
public Symbol(String name, Symbol type) {
this.name = name;
this.type = type;
}
}
The type is a pointer to another symbol; e.g., IntType or, later, a user-defined type.
Your program doesn't need an explicit stack of Scope objects to push/pop from upon entry/exit from a scope because of the parent point--there is an implied stack.
The following portions of a simplified C grammar with actions describes how you manage the scopes and define program entities (it does not deal with AST creation).
{
Scope globalScope = new Scope(null);
Scope currentScope = globalScope; }
func
: t=type ID
{currentScope.defineFunction($ID.text,$t.type);} {currentScope = new Scope(currentScope);} arguments
slist
{currentScope = currentScope.pop();} ;
arguments
: '(' args ')'
;
stat: ifStat
| slist
| varDefinition
| ...
;
varDefinition
: t=type ID SEMI {currentScope.defineVariable($ID.getText(),$t.type);}
;
type returns [Symbol type]
: 'int' {return theIntTypeSymbol;} | 'float' {return theFloatTypeSymbol;}
...
;
ifStat
: 'if' '(' expr ')' stat
;
slist
: {currentScope = new Scope(currentScope);} '{' stat+ '}'
{currentScope = currentScope.pop();} ;
At this point, you have a properly managed symbol table. Notice that there is no overall symbol table in my implementation. Scopes and their associated symbols evaporate as scopes are exited. (Once you've exited a scope, you cannot see those variables anymore, so why bother keeping them around?) That implies that the symbol lookups (which return a Symbol) must hold on to the symbol definition or we've wasted our time building up the symbol table information. Either you have a one-pass system that immediately uses the looked up symbol or you must store the symbol reference somewhere. That "somewhere" would mean your AST. Later tree walks could access a symbol definition pointer in any AST nodes that reference a symbol. You can also store pointers to Scope objects at appropriate places in the tree like at SLIST and FUNCTION_DEF nodes.
That brings up another point. Should you do symbol table management in the parser or in a tree parser phase? That depends on how difficult the symbol semantics are. If the language allows forward references, you have no choice but to do some kind of two pass system. Another complexity is context-sensitive parsing. If you cannot parse the input properly without symbol table information (i.e., you need to see user-defined typedefs), then you must do the symbol table management within the parser itself.
Personally, even if I don't have forward references, I like to have each phase of my translator as simple as possible, therefore, I make the parser just build trees (also making it more reusable). Then I have a separate tree walking phase that does symbol table management. It is much faster to do in the parser as you don't have to walk again, but it can be harder to read.
Regardless of which way you go, your goal should be to have AST nodes that reference symbols directly or indirectly point to the Symbol definition. The Symbol can point at the AST that defines that variable even if it's a forward reference. The AST node that defines a variable would point back to the Symbol definition. The Symbol is like a condensed form of the actual definition.
Here are some useful classes. Typically you will distinguish the kinds of symbols by a subclass such as VariableSymbol or FunctionSymbol. The predefined types also have their own classes.
Symbol { String name; Symbol type; Scope scope; Tree definition; }
subclass VariableSymbol
subclass FunctionSymbol { List<Symbol> arguments; }
subclass IntType
subclass FloatType
subclass PtrToType { Symbol ptrToWhat; }
...
Symbol objects just map a name to a type (variable) or return type (function). (I've hinted at what you need for pointers to types; turns out to be a tree also). Symbols should also know what scope they live in. The definition AST pointer is used to tell the symbol where in the tree it is defined. When you see a variable reference look it up. If we wanted to just verify that variables and functions were defined, the following would suffice using validating semantic predicates.
expr: ... ;
atom: INT
| FLOAT
| ID {currentScope.lookup($ID.text)!=null}?
| ID '(' exprList ')' {currentScope.lookup($ID.text)!=null}?
| '(' expr ')'
...
;
Let's pretend we're doing symbol table management in the tree parser now. Consider:
If you want to store symbols in your AST, you want your AST to look something like this:

where the AST ID definition and reference nodes point at the symbol table definition object.
The code would look like:
...
varDefinition
: ^(VARDEF ID t=type)
{
Symbol s = currentScope.defineVariable($ID.text,$t.type);
s.definition = $VARDEF; $ID.symbol = s;
}
;
where you would have to define your own AST that could deal with Symbol pointers.
References would be handled like this:
atom: INT
| FLOAT
| ID
{
VariableSymbol s = (VariableSymbol)currentScope.lookup($ID.text);
$ID.symbol = s;
}
| ^(FUNC_CALL ID exprList)
{
FunctionSymbol f = (FunctionSymbol)currentScope.lookup($ID.text);
$FUNC_CALL.symbol = f;
}
...
;
You can figure out the corresponding definition node for a symbol reference node by following the AST symbol pointer back to the Symbol object and then back into the tree via the definition pointer.