package compiler.util;
import static grammar.GrammarDSL.endOfInput;
import static grammar.GrammarDSL.seq;
import static trees.MatchSpec.expr;
import static trees.MatchSpec.hasMatchAtPos;
import static trees.MatchSpec.rule;
import static util.StringUtils.unescape;
import java.util.ArrayList;
import java.util.List;
import driver.Context;
import driver.Hints;
import grammar.Expression;
import grammar.Expression.Rule;
import grammar.Grammar;
import parser.Match;
import parser.Matcher;
import source.Source;
import source.SourceString;
import trees.MatchSpec;
import util.ArraySlice;
/**
* See the section of my thesis titled "(Quasi)quotation" to learn more about
* quotation and quasiquotation.
*/
public class Quoter
{
/*****************************************************************************
* This call is meant to be made from Java code and does not require
* preliminary source preprocessing. The contents of $code should follow the
* syntax of {@link grammar.java._E_MacroDefinitions#dynamicSourceFragment} (specifying
* inserts with "#1", "#2", etc; where the number is an index into $matches).
*/
public static Match dynamicQuote(String rule, String code, Object... inserts)
{
return new DynamicQuoteMethod().dynamicQuote(rule, code, inserts);
}
/****************************************************************************/
private static class DynamicQuoteMethod
{
// // GLOBALS
// ===============================================================
String code; // [final] original quote string
Object[] inserts; // [final] objects to be inserted
List<MatchSpec> specs; // verify that inserted matches do parse back
// index diff for current marker between $code and $expansion
int diff = 0;
// // PER MARKER
// ============================================================
Match marker; // current marker
int number; // insert (of $inserts) index by $marker
boolean isSplice; // is the marker for splicing (match array insertion)?
Object insert; // $inserts[$number]
String insertion; // $insert, converted to string according to $marker
/******************************************************************h********/
Match dynamicQuote(String rule, String code, Object... inserts)
{
StringBuffer expansion = new StringBuffer(code);
Matcher matcher = new Matcher(new SourceString(code));
this.code = code;
this.inserts = inserts;
this.specs = new ArrayList<>(inserts.length);
// The rule can match any string.
matcher.matches(Context.get().grammar().rule("dynamicSourceFragment"));
for (Match marker : matcher.match().all(rule("insertMarker")))
if (!marker.has(rule("backslash"))) // escaped marker
{
updatePerMarkerVariables(marker);
expansion.replace(marker.begin + diff, marker.end + diff, insertion);
diff = diff - marker.length() + insertion.length();
}
return primitiveQuote(rule, expansion.toString(), specs);
}
/**************************************************************************/
void updatePerMarkerVariables(Match marker)
{
number = Integer.parseInt(marker.first(rule("parseIntNumber")).string());
this.marker = marker;
checkNumber();
String str = marker.string();
isSplice = str.startsWith("#@");
insert = inserts[number - 1];
checkType();
insertion = getInsertionString();
}
/**************************************************************************/
void checkType()
{
if (isSplice && !insert.getClass().isArray()) {
throw new Error("The parameter supplied to dynamicQuote() for the "
+ "splice marker " + marker.string() + " should be an array, in ["
+ code + "].");
}
}
/**************************************************************************/
void checkNumber()
{
if (number > inserts.length) {
throw new Error("Insert number in source fragment is too high: "
+ number + " when the max is " + inserts.length + ".");
}
}
/*****************************************************************************
* Returns $insert, converted to string according to $marker. Adds items in
* $specs for $insert.
*/
String getInsertionString()
{
return isSplice
? getSpliceString((Object[]) insert, getDelimiters(marker))
: insertToString(insert);
}
/***************************************************************************
* Returns the delimiters of a spliced inserts.
*/
private String[] getDelimiters(Match marker)
{
assert isSplice;
Match[] delimMatches = marker.all(rule("spliceDelimiter"));
String[] delimiters = new String[3];
for (int i = 0; i < 3; ++i)
{
delimiters[i] = unescape(delimMatches[i].string());
delimiters[i] = delimiters[i].replaceAll("\\\\\\|", "|");
// We replace \|, but this is a regexp inside a java string.
}
return delimiters;
}
/***************************************************************************
* Converts an inserted object to a string holding the code it represents.
* Uses {@link Match#string()} for matches, {@link Object#toString()}
* otherwise.
*/
private String insertToString(Object insert)
{
if (insert instanceof Match) {
Match match = (Match) insert;
specs.add(hasMatchAtPos(match, marker.begin + diff));
return match.string();
}
else {
return insert.toString();
}
}
/***************************************************************************
* Like {@link #getInsertionString()}, but specialized for match splices.
*/
String getSpliceString(Object[] inserts, String[] seps)
{
if (inserts.length == 0) {
return "";
}
// Keep $diff accurate for $specs.
int old_diff = diff;
diff += seps[0].length();
StringBuilder builder = new StringBuilder(seps[0]);
for (Object insert : inserts)
{
String str = insertToString(insert);
builder.append(str);
builder.append(seps[1]);
diff += str.length() + seps[1].length();
}
diff = old_diff;
builder.delete(builder.length() - seps[1].length(), builder.length());
builder.append(seps[2]);
return builder.toString();
}
}
/*****************************************************************************
* Primitive form of quotation. Parses $code according to the rule named
* $ruleName. Verifies that the resulting match satisfies the specifications
* in $specs.
*/
public static Match primitiveQuote(String ruleName, String code,
List<MatchSpec> specs)
{
Grammar grammar = Context.get().grammar();
Rule rule = grammar.rule(ruleName);
Expression wrap = grammar.clean(seq(rule, endOfInput));
Source source = new SourceString(code);
Matcher matcher = new Matcher(source);
if (!matcher.matches(wrap)) {
throw new Error("The quotation '" + rule + " [" + code
+ "]' does not yield a valid parse.");
}
Match result = matcher.match();
Hints.get().hintSource(source);
for (MatchSpec spec : specs) {
if (!spec.matches(result)) {
throw new Error("The match tree obtained by parsing \"" + code
+ "\" did not match the specification:\n" + spec);
}
}
Hints.get().endHintSource();
return result.first(expr(rule));
}
/****************************************************************************/
public static Match primitiveQuote(String ruleName, String code,
MatchSpec... specs)
{
return primitiveQuote(ruleName, code, new ArraySlice<>(specs));
}
}