Es una representación abstracta de la sintaxis de un código fuente y es abstracta por qué representa con detalle la sintaxis de un lenguaje de programación
La Idea de una AST es que cada nodo se refiere a una construcción del código fuente por ejemplo existe el nodo If, While, asiganción. etc.
Al final de las hojas se encuentran los terminales de una gramática por ejemplo las variables, operadores, literales etc.
Ahora, porque AST y no colocar las acciones semánticas en la gramática:
  1. que todo necesitamos de las Acciones Semánticas para construir el árbol (pero para nada más)
  2. Raramente las herramientas para generar parser soportan atributos heredados y trasformar la gramática o utilizar la una pila es un tanto complicado.
  3. si se quiere interpretar un lenguaje la única forma (sin quebrarnos la cabeza) es con el AST.
  4. si se tiene N gramáticas es más fácil escribir acciones semánticas para construir el AST que traducir directamente.
Así por ejemplo el árbol de la gramática (usual) de asignación
asingacion::= ID ‘=’ Exp:a1 {:RESULT=new NodoAsignacion(ID,a1);:}
exp::= Exp:a1 ‘+’ literal:a2 {:RESULT=new NodoExp(a1,’+’,a2) :} //el Símbolo + podría cambiar por cualquier operador binario. se construye de la misma manera
|literal:a3
literal::= Num {:RESULT=new NodoLiteral(Num);:}
|ID {:RESULT=new NodoLiteral(ID);:}
Se podría agregar información extra (A las clases) como por ejemplo el No Línea, No Columna para poder identificar los Errores cuando se esté generando código o interpretando. Así como también apuntadores a la Tabla de Símbolos y al manejador de Errores.
c=a+5;
El árbol quedaría de la siguiente manera:

Note que el árbol se construirá de acuerdo a la gramática del lenguaje. Ahora para poder interpretar o genera código se haría un recorrido (o varios) en orden del AST.
He aquí un repositorio de un ejemplo (espero bastante completo) para que se hagan una idea de cómo implementa un AST de acuerdo a sus necesidades.
http://code.google.com/p/compi2jbattleship/
Acerca del ejemplo es un juego de naves que compila un subconjunto del lenguaje de Java hacia Código de 3 direcciones (TAC: Three Address Code) y luego interpreta El TAC para poder jugar de acuerdo el código.

En este post presento un interprete muy simple que puede ser usado como base para la construcción de algo mas complejo, la idea fundamental en ese, es ayudar a comprender el funcionamiento de Java CUP, un generador de analizadores sintácticos LALR para Java y JFLEX un generador de analizadores léxicos basados en tabla también para Java.
El interprete consiste en una aplicación por consola que recibe como parámetro el path del archivo que se desea interpretar, y simplemente ejecuta las instrucciones del archivo en forma secuencial.
El lenguaje permite describir operaciones matemáticas, en notación polaca usando la sintaxis siguiente:

push 10 : para ingresar un numero a la pila

print :  para extraer un numero de la pila y mostrarlo en pantalla

add: suma los dos números mas arriba de la pila

sub: resta los dos números mas arriba de la pila

mult: multiplicación de los dos números mas arriba en la pila

div: división de los dos números mas arriba en la pila

Un ejemplo de archivo de entrada para la expresion  ( ( 10 + 5 ) + 15 ) * 2

push 10
push 5
add
push 15
add
push 2
mult
print

la salida para este ejemplo

la gramatica para este interprete es la siguiente

package test;

import java_cup.runtime.*;

parser code {:

public CalculadoraDePila calc = new CalculadoraDePila();

:}

terminal TOKEN_ADD, TOKEN_SUB, TOKEN_MULT, TOKEN_DIV, TOKEN_PUSH, TOKEN_PRINT;
terminal String TOKEN_NUMBER;

non terminal Documento;

non terminal Lista;

non terminal Elemento;

Documento ::= Lista {: System.out.println("DOCUMENTO OK"); :};

Lista ::= Lista Elemento {: :}
| Elemento {: :};

Elemento ::= TOKEN_ADD {: parser.calc.add(); :}
| TOKEN_SUB {: parser.calc.sub(); :}
| TOKEN_MULT {: parser.calc.mult(); :}
| TOKEN_DIV {: parser.calc.div(); :}
| TOKEN_PRINT {: parser.calc.print(); :}
| TOKEN_PUSH TOKEN_NUMBER:n
{:
Integer numero = Integer.parseInt( n );
parser.calc.push( numero );
:};

y para el analizador lexico

package test;

import java_cup.runtime.Symbol;

%%

%{

%}

%line
%char

%cup

%%

[0-9]+ { return new Symbol( sym.TOKEN_NUMBER , yytext() ); }
"push" { return new Symbol( sym.TOKEN_PUSH , yytext() ); }
"add" { return new Symbol( sym.TOKEN_ADD , yytext() ); }
"sub" { return new Symbol( sym.TOKEN_SUB , yytext() ); }
"div" { return new Symbol( sym.TOKEN_DIV , yytext() ); }
"mult" { return new Symbol( sym.TOKEN_MULT , yytext() ); }
"print" { return new Symbol( sym.TOKEN_PRINT , yytext() ); }

[\n\r\t ]+ { }

. { System.out.println( "Caracter no Esperado, ERROR LEXICO " + yytext() ); }

Código fuente en NetBeans

Esta entrada tiene como objetivo mostrar una forma simple de utilizar JFlex en Windows para generar analizadores léxicos que luego pueden ser utilizados en aplicaciones de Java, se incluye una guía en pdf que muestra los pasos necesarios para construir un proyecto en JDeveloper 11g, desarrollar el archivo de especificación del analizador lexico que luego es utilizado por el ejecutable de JFlex para generar el código fuente del analizador lexico (por defecto Yylex.java), luego se crea código de prueba en una aplicación mínima de Java (solo método main) en donde se crea un objeto de la clase del analizador lexico y luego se utiliza para analizar un archivo simple de prueba, token por token mostrando el tipo de cada token (identificador, codigo).

archivos necesarios (proyecto de JDeveloper, guia en pdf)