package compiler;
import static trees.MatchSpec.or;
import static trees.MatchSpec.rule;
import static util.StringUtils.endsWithWhitespace;
import static util.StringUtils.escape;
import java.util.ArrayList;
import java.util.List;
import compiler.macros.MacroInterface;
import compiler.util.Quoter;
import parser.Match;
import trees.MatchSpec;
/**
* Expanding the "quotation" rule (holding either a simple quotation or a
* quasiquotation).
*/
public class QuotationMacro implements MacroInterface
{
/****************************************************************************/
public Match expand(Match quotation)
{
return new QuotationMacroImplem().expand(quotation);
}
}
/******************************************************************************/
class QuotationMacroImplem
{
// Don't recreate the match specification each time.
private static final MatchSpec UNQUOTATION = rule("unquotation");
private static final MatchSpec QUASIQUOTATION = rule("quasiquotation");
private static final MatchSpec SQUOTATION = rule("simpleQuotation");
private static final MatchSpec ESCAPED_QEND = rule("escapedQEndMarker");
private static final MatchSpec UNARY = rule("unaryExpression");
private static final MatchSpec BACKSLASH = rule("backslash");
private static final MatchSpec UNQUOT_OR_UNARY = or(UNQUOTATION, UNARY);
private static final MatchSpec SOURCE_SUB =
or(SQUOTATION, QUASIQUOTATION, UNQUOTATION, ESCAPED_QEND);
private static final MatchSpec HASH_LIKE =
or(rule("hash"), rule("hashat"));
/****************************************************************************/
static final String PRIMITIVE_QUOTE = "compiler.util.Quoter.primitiveQuote";
/****************************************************************************/
static final String DYNAMIC_QUOTE = "compiler.util.Quoter.dynamicQuote";
/*****************************************************************************
* The expansion of the macro.
*/
private StringBuilder expansion;
/*****************************************************************************
* Difference in index between the expansion and the source the quotation was
* taken from, for the next unquotation to be processed.
*/
private int diff;
/*****************************************************************************
* Index for the next insert marker to be inserted into the expansion.
*/
private int insertMarkerCounter = 1;
/*****************************************************************************
* Indicates if nesting level of quotations/unquotations. Used to determine
* if we are directly under the outermost quotation to remove escapes there.
* Not be confused with depth.
*/
private int nesting = 0;
/*****************************************************************************
* Inserts extracted from the source fragment.
*/
private final List<Match> inserts = new ArrayList<>();
/****************************************************************************/
public Match expand(Match quotation)
{
String ruleName = quotation.first(rule("identifier")).string();
Match sourceFragment = quotation.first(rule("sourceFragment"));
/* Need originalString() to use Match#begin and Match#end as indications.
* Trimming removes end whitespace (there is no starting whitespace because
* of the grammar rule). */
expansion = new StringBuilder(sourceFragment.originalString().trim());
diff = - sourceFragment.begin;
replaceUnquotations(quotation, 0);
/* Escape the expansion to make it suitable to appear within a string
* literal. */
expansion.replace(0, expansion.length(), escape(expansion.toString()));
String expandFunc = inserts.isEmpty() ? PRIMITIVE_QUOTE : DYNAMIC_QUOTE;
expansion.insert(0, expandFunc + "(\"" + ruleName + "\", \"");
expansion.append("\"");
for (Match insert : inserts) {
expansion.append(", (Object)"); // avoids ambiguity with only one insert
expansion.append(insert.string());
}
expansion.append(")");
return Quoter.primitiveQuote("unaryExpression", expansion.toString());
}
/*****************************************************************************
* Recursively replace unquotations in the source fragment with insertion
* markers.
*/
private void replaceUnquotations(Match match, int depth)
{
for (Match sub : match.all(SOURCE_SUB))
{
/* We use child() to recurse. It is always appropriate since simpleQuotation,
* quasiquotation and unquotation are all rules. */
++nesting;
if (sub.is(QUASIQUOTATION)) {
replaceUnquotations(sub.child(), depth + 1);
}
else if (sub.is(SQUOTATION)) {
if (depth > 0) {
replaceUnquotations(sub.child(), depth);
}
else /* depth == 0 */ {
if (nesting == 1)
for (Match escape : sub.child().all(SOURCE_SUB))
if (escape.is(ESCAPED_QEND)) {
unescapeEscapedEndMarker(escape);
}
}
}
else if (sub.is(ESCAPED_QEND)) {
if (nesting == 2) {
unescapeEscapedEndMarker(sub);
}
}
else /* UNQUOTATION */ {
processUnquotation(sub.child(), depth - 1);
}
--nesting;
}
}
/****************************************************************************/
private void unescapeEscapedEndMarker(Match escape)
{
expansion.replace(escape.begin + diff, escape.end + diff,
escape.string().substring(1));
diff -= 1;
}
/*****************************************************************************
* If $unquotation is at depth 0, replace it by an insertion marker.
*/
private void processUnquotation(Match unquotation, int depth)
{
boolean escaped = unquotation.has(
unquotation.firstBeforeFirst(BACKSLASH, HASH_LIKE));
if (escaped) { depth += 1; }
if (depth == 0) {
applyUnquotation(unquotation);
/* The unquoted expression will appear as variadic parameter to
* dynamicQuote(). If the expression contains nested
* quotations/unquotations, those will be handled recursively by the macro
* expander. */
}
else {
replaceUnquotations(unquotation, depth);
}
}
/*****************************************************************************
* Add the unquoted expression to {@link #inserts} and replace the unquotation
* by an insertion marker.
*/
private void applyUnquotation(Match unquot)
{
Match unary = unquot.first(UNQUOT_OR_UNARY);
if (unary.is(UNQUOTATION)) {
throw new Error("Unquotation with negative depth.");
}
inserts.add(unary);
String str = unquot.string();
boolean isSplice = str.startsWith("#@");
String hashLike = isSplice
? "#@" + unquot.first(rule("spliceDelimiters")).string()
: "#";
boolean spaced = endsWithWhitespace(unquot.originalString());
String marker = hashLike + insertMarkerCounter++ + (spaced ? " " : "");
expansion.replace(unquot.begin + diff, unquot.end + diff, marker);
diff = diff - unquot.length() + marker.length();
}
}