package freeboogie.ast.gen;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.Stack;
import java.util.TreeSet;
import java.util.logging.Logger;
import freeboogie.util.Err;
/**
* The template parser. This is where the output is produced.
* By default the output is set to {@code null} which means that
* the beginning of a template can contain arbitrary comments.
* The <tt>\file</tt> macro switches the output to another
* destination. (A common trick is to use /dev/stdout as a sink
* to inform the user about the progress of the template processing.)
*
* Some macros must be nested in others. For example \class_name
* must be nested in \classes or \normal_classes or \abstract_classes.
* If the nesting is incorrect a warning is printed on the console
* and <WRONG_MACRO> goes to the output. If there are nested \classes
* macros then only the innermost one is considered. (In most
* applications nested list macros should not be needed.)
*
* TODO: consider adding an optional parameter for macros that indicates
* a nesting level such that, for example, the following is legal
* \classes{(\classes{\ClassName[0],\ClassName[1]})}
* and prints all pairs of class names.
*
* TODO If nested list macros are deemed unnecessary then switch the
* context stacks to single (nullable) elements.
*
* TODO The duplicated code is a bit too much for my taste.
*
* TODO I may want to add macro definitions such as
* \def\Type{\if_primitive{\if_enum{\ClassName.}{}\Membertype}{\MemberType}}
* It gets tedious to write it.
*
* @author rgrig
* @author reviewed by TODO
*/
public class TemplateParser {
/*
* The function |processTop| is the main loop. It reads a
* token and distributes the work to various |process*| methods.
* It is also responsible for stopping when a certain closing
* } or ] is met (and it reads it). The variables |curlyCnt|
* and |bracketCnt| count the nesting (since the beginning
* of the template) and are used to identify on which ] or }
* we should stop. Notice that the user shouldn't use unbalanaced
* paranthesis in the template for this scheme to work.
*
* The stacks |*Context| contain information about the nested
* macros seen in the input.
*/
private static final Logger log = Logger.getLogger("freeboogie.ast.gen");
private TemplateLexer lexer;
private Grammar grammar;
private Stack<AgClass> classContext;
private Stack<AgMember> memberContext;
private Stack<AgEnum> enumContext;
private Stack<String> valueContext;
private Stack<String> invariantContext;
private Writer output;
private int curlyCnt; // counts {} nesting
private int bracketCnt; // counts [] nesting
private TemplateToken lastToken;
private boolean balancedWarning = true;
// classes are processed in alphabetical order
private Set<AgClass> orderedClasses;
private Set<AgClass> abstractClasses;
private Set<AgClass> normalClasses;
/**
* @param fileName the name of the template file
* @throws FileNotFoundException if the template file is not found
*/
public TemplateParser(String fileName) throws FileNotFoundException {
FileInputStream fis = new FileInputStream(fileName);
CharStream cs = new CharStream(fis, fileName);
lexer = new TemplateLexer(cs);
output = null;
lastToken = null;
classContext = new Stack<AgClass>();
memberContext = new Stack<AgMember>();
enumContext = new Stack<AgEnum>();
valueContext = new Stack<String>();
invariantContext = new Stack<String>();
orderedClasses = null;
abstractClasses = null;
normalClasses = null;
}
/**
* Processes the current template using grammar {@code g}.
* @param g the grammar
* @throws IOException
*/
public void process(Grammar g) throws IOException {
grammar = g;
processTop(Integer.MAX_VALUE, Integer.MAX_VALUE);
if (output != null) output.flush();
}
/*
* For now I will enforce {} and [] to be balanced pretty much
* everywhere.
*
* The function dispatches the work to the appropriate worker.
* It also takes care of stoping when a } or a ] with a given
* nesting level is seen.
*/
private void processTop(int curlyStop, int bracketStop) throws IOException {
readToken();
while (lastToken != null) {
switch (lastToken.type) {
case FILE:
processFile(); break;
case CLASSES:
processClasses(); break;
case IF_ABSTRACT:
processIsAbstract(); break;
case ABSTRACT_CLASSES:
processAbstractClasses(); break;
case NORMAL_CLASSES:
processNormalClasses(); break;
case CLASS_NAME:
processClassName(); break;
case BASE_NAME:
processBaseName(); break;
case MEMBERS:
processMembers(); break;
case MEMBER_TYPE:
processMemberType(); break;
case MEMBER_NAME:
processMemberName(); break;
case IF_PRIMITIVE:
processIfPrimitive(); break;
case IF_NONNULL:
processIfNonnull(); break;
case IF_ENUM:
processIfEnum(); break;
case CHILDREN:
processChildren(); break;
case PRIMITIVES:
processPrimitives(); break;
case ENUMS:
processEnums(); break;
case ENUM_NAME:
processEnumName(); break;
case VALUES:
processValues(); break;
case VALUE_NAME:
processValueName(); break;
case INVARIANTS:
processInvariants(); break;
case INV:
processInv(); break;
default:
if (curlyStop == curlyCnt || bracketStop == bracketCnt) return;
write(lastToken.rep);
}
if (curlyCnt == 0 && bracketCnt == 0) lexer.eat();
readToken();
}
}
/*
* Reads { file_name } and makes output point to file_name.
* If the file cannot be open then switch to a null output
* and give a warning.
*/
private void processFile() throws IOException {
readToken();
if (lastToken.type != TemplateToken.Type.LC) {
err("Hey, \\file should be followed by {");
Err.help("I'm gonna stop producing output.");
switchOutput(null);
skipToRc(curlyCnt, true);
return;
}
StringWriter sw = new StringWriter();
switchOutput(sw);
processTop(curlyCnt - 1, Integer.MAX_VALUE);
String fn = sw.toString().replaceAll("\\s+", "_");
try {
FileWriter fw = new FileWriter(fn);
switchOutput(fw);
log.info("The output goes to the file " + fn);
} catch (IOException e) {
err("Cannot write to file " + fn);
Err.help("I'm gonna stop producing output.");
switchOutput(null);
}
}
private <T> void processList(Collection<T> set, Stack<T> stack) throws IOException {
readToken();
String separator = "";
if (lastToken.type == TemplateToken.Type.LB) {
readToken();
if (lastToken.type != TemplateToken.Type.OTHER) {
err("Sorry, you can't use any funny stuff as a separator.");
skipToRc(curlyCnt, true);
return;
}
separator = lastToken.rep;
readToken();
if (lastToken.type != TemplateToken.Type.RB) {
err("The separator is not properly closed by ].");
skipToRb(bracketCnt - 1, true);
}
if (lastToken.type != TemplateToken.Type.LC)
readToken();
}
if (lastToken.type != TemplateToken.Type.LC) {
err("There should be a { after a list macro.");
skipToRc(curlyCnt - 1, true);
return;
}
if (set.isEmpty()) skipToRc(curlyCnt - 1, false);
int i = 0; // TODO is there another way to check if I'm looking at the last?
for (T el : set) {
if (i != 0) {
lexer.rewind();
write(separator);
++curlyCnt;
}
if (++i != set.size()) lexer.mark();
stack.add(el);
processTop(curlyCnt - 1, Integer.MAX_VALUE);
stack.pop();
}
}
private void processClasses() throws IOException {
splitClasses();
processList(orderedClasses, classContext);
}
private void processYesNo(boolean yes) throws IOException {
if (!yes) skipToRc(curlyCnt, false);
readToken();
if (lastToken.type != TemplateToken.Type.LC) {
err("An if macro should be followed by {yes}{no}.");
Err.help("I'll act as if <" + lastToken.rep + "> was {.");
}
processTop(curlyCnt - 1, Integer.MAX_VALUE);
if (yes) skipToRc(curlyCnt, false);
}
private void processIsAbstract() throws IOException {
if (!checkContext(classContext)) {
skipToRc(curlyCnt, true);
skipToRc(curlyCnt, true);
return;
}
processYesNo(classContext.peek().members.isEmpty());
}
private void splitClasses() {
if (abstractClasses != null) return;
orderedClasses = new TreeSet<AgClass>();
abstractClasses = new TreeSet<AgClass>();
normalClasses = new TreeSet<AgClass>();
for (AgClass c: grammar.classes.values()) {
orderedClasses.add(c);
if (c.members.isEmpty())
abstractClasses.add(c);
else
normalClasses.add(c);
}
}
private void processAbstractClasses() throws IOException {
splitClasses();
processList(abstractClasses, classContext);
}
private void processNormalClasses() throws IOException {
splitClasses();
processList(normalClasses, classContext);
}
private <T> boolean checkContext(Stack<T> context) throws IOException {
if (context.isEmpty()) {
err("Macro used in a wrong context.");
write("<WRONG_MACRO>");
return false;
}
return true;
}
private void processClassName() throws IOException {
if (checkContext(classContext))
writeId(classContext.peek().name, lastToken.idCase);
}
private void processBaseName() throws IOException {
if (checkContext(classContext))
writeId(classContext.peek().base, lastToken.idCase);
}
private void processMembers() throws IOException {
if (!checkContext(classContext)) {
skipToRc(curlyCnt, true);
return;
}
processList(classContext.peek().members, memberContext);
}
private void processMemberType() throws IOException {
if (checkContext(memberContext))
writeId(memberContext.peek().type, lastToken.idCase);
}
private void processMemberName() throws IOException {
if (checkContext(memberContext))
writeId(memberContext.peek().name, lastToken.idCase);
}
private void processIfPrimitive() throws IOException {
if (!checkContext(memberContext)) {
skipToRc(curlyCnt, true);
skipToRc(curlyCnt, true);
return;
}
processYesNo(memberContext.peek().primitive);
}
private void processIfNonnull() throws IOException {
if (!checkContext(memberContext)) {
skipToRc(curlyCnt, true);
skipToRc(curlyCnt, true);
return;
}
processYesNo(memberContext.peek().nonNull);
}
private void processIfEnum() throws IOException {
if (!checkContext(memberContext)) {
skipToRc(curlyCnt, true);
skipToRc(curlyCnt, true);
return;
}
processYesNo(memberContext.peek().isenum);
}
private void processChildren() throws IOException {
if (!checkContext(classContext)) {
skipToRc(curlyCnt, true);
return;
}
List<AgMember> children = new ArrayList<AgMember>(23);
for (AgMember m : classContext.peek().members)
if (!m.primitive) children.add(m);
processList(children, memberContext);
}
private void processPrimitives() throws IOException {
if (!checkContext(classContext)) {
skipToRc(curlyCnt, true);
return;
}
List<AgMember> primitives = new ArrayList<AgMember>(23);
for (AgMember m : classContext.peek().members)
if (m.primitive) primitives.add(m);
processList(primitives, memberContext);
}
private void processEnums() throws IOException {
if (!checkContext(classContext)) {
skipToRc(curlyCnt, true);
return;
}
processList(classContext.peek().enums, enumContext);
}
private void processEnumName() throws IOException {
if (checkContext(enumContext))
writeId(enumContext.peek().name, lastToken.idCase);
}
private void processValues() throws IOException {
if (!checkContext(enumContext)) {
skipToRc(curlyCnt, true);
return;
}
processList(enumContext.peek().values, valueContext);
}
private void processValueName() throws IOException {
if (checkContext(valueContext))
writeId(valueContext.peek(), lastToken.idCase);
}
private void processInvariants() throws IOException {
if (!checkContext(classContext)) {
skipToRc(curlyCnt, true);
return;
}
processList(classContext.peek().invariants, invariantContext);
}
private void processInv() throws IOException {
if (checkContext(invariantContext))
write(invariantContext.peek());
}
private void skipToRc(int cnt, boolean warn) throws IOException {
skipToR(cnt, Integer.MAX_VALUE, warn);
}
private void skipToRb(int cnt, boolean warn) throws IOException {
skipToR(Integer.MAX_VALUE, cnt, warn);
}
/*
* This reads the input until either it finishes or the } or ]
* with the specified nesting level is encountered.
*/
private void skipToR(int curlyStop, int bracketStop, boolean w) throws IOException {
StringBuilder sb = new StringBuilder();
while (true) {
readToken();
if (lastToken == null) break;
sb.append(lastToken.rep);
if (lastToken.type == TemplateToken.Type.RC
&& curlyStop == curlyCnt) break;
if (lastToken.type == TemplateToken.Type.RB
&& bracketStop == bracketCnt) break;
}
if (w) Err.help("I'm skipping: " + sb);
}
private void readToken() throws IOException {
lastToken = lexer.next();
if (lastToken == null) return;
log.finer("read token <" + lastToken.rep + "> of type " + lastToken.type);
if (lastToken.type == TemplateToken.Type.LB) ++bracketCnt;
if (lastToken.type == TemplateToken.Type.RB) --bracketCnt;
if (lastToken.type == TemplateToken.Type.LC) ++curlyCnt;
if (lastToken.type == TemplateToken.Type.RC) --curlyCnt;
if (balancedWarning && (curlyCnt < 0 || bracketCnt < 0)) {
err("You are on thin ice.");
Err.help("I don't guarantee what happens if you use unbalaced [] or {}.");
balancedWarning = false;
}
}
private void switchOutput(Writer newOutput) throws IOException {
if (output != null) output.flush();
output = newOutput;
if (output == null)
log.fine("Output is turned off.");
}
/*
* Writes |id| using the case convention |cs|.
*/
// candidate for memoization
private void writeId(String id, TemplateToken.Case cs) throws IOException {
if (cs == TemplateToken.Case.ORIGINAL_CASE) {
write(id);
return;
}
StringBuilder res = new StringBuilder(id.length());
boolean first = true;
boolean prevIs_ = true;
boolean prevIsUpper = false;
for (int i = 0; i < id.length(); ++i) {
char c = id.charAt(i);
if (c == '_') prevIs_ = true;
else {
boolean thisIsUpper = Character.isUpperCase(c);
if (prevIs_ || (thisIsUpper && !prevIsUpper)) {
// beginning of word
switch (cs) {
case CAMEL_CASE:
if (first) res.append(Character.toLowerCase(c));
else res.append(Character.toUpperCase(c));
break;
case PASCAL_CASE:
res.append(Character.toUpperCase(c));
break;
case LOWER_CASE:
if (!first) res.append('_');
res.append(Character.toLowerCase(c));
break;
case UPPER_CASE:
if (!first) res.append('_');
res.append(Character.toUpperCase(c));
break;
default:
Err.fatal("Don't know which case to use for " + id);
}
} else {
// the rest of letters
switch (cs) {
case UPPER_CASE:
res.append(Character.toUpperCase(c));
break;
default:
res.append(Character.toLowerCase(c));
}
}
first = false;
prevIs_ = false;
prevIsUpper = thisIsUpper;
}
}
write(res.toString());
}
/*
* Sends |s| to the |output|.
*/
private void write(String s) throws IOException {
if (output != null) {
output.write(s);
}
}
private void err(String e) {
Err.error(lexer.getName() + lexer.getLoc() + ": " + e);
}
}