package org.reasm.m68k.assembly.internal;
import java.util.ArrayList;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import javax.annotation.concurrent.Immutable;
import javax.annotation.meta.When;
import org.reasm.commons.source.Syntax;
import org.reasm.m68k.messages.MultipleOperandPacksInMacroDefinitionErrorMessage;
import org.reasm.m68k.source.M68KParser;
import org.reasm.source.MacroInstantiation;
import org.reasm.source.SourceLocation;
import ca.fragag.text.DocumentReader;
import ca.fragag.text.RangedCharSequenceReader;
/**
* A user-defined macro.
*
* @author Francis Gagné
*/
@Immutable
class Macro extends Mnemonic {
@Immutable
private static class OperandFromEndSubstitutionSource extends SubstitutionSource {
private static final int MAX_CACHE_SIZE = 256;
@Nonnull
private static final ArrayList<OperandFromEndSubstitutionSource> CACHE = new ArrayList<>();
@Nonnull
static OperandFromEndSubstitutionSource get(int operandIndex) {
int cacheIndex = ~operandIndex;
if (cacheIndex >= MAX_CACHE_SIZE) {
return new OperandFromEndSubstitutionSource(operandIndex);
}
while (cacheIndex >= CACHE.size()) {
CACHE.add(new OperandFromEndSubstitutionSource(~CACHE.size()));
}
return CACHE.get(cacheIndex);
}
// operandIndex is always a negative value.
// -1 means the last operand, -2 means the second to last operand, etc.
@Nonnegative(when = When.NEVER)
private final int operandIndex;
private OperandFromEndSubstitutionSource(@Nonnegative(when = When.NEVER) int operandIndex) {
this.operandIndex = operandIndex;
}
@Override
String substitute(M68KAssemblyContext context, int macroCounter, Macro macro) {
final int realOperandIndex = context.numberOfOperands + this.operandIndex;
if (realOperandIndex >= 0) {
return context.getOperandText(realOperandIndex);
}
return "";
}
}
@Immutable
private static final class OperandSubstitutionSource extends SubstitutionSource {
private static final int MAX_CACHE_SIZE = 256;
@Nonnull
private static final ArrayList<OperandSubstitutionSource> CACHE = new ArrayList<>();
@Nonnull
static OperandSubstitutionSource get(int operandIndex) {
if (operandIndex >= MAX_CACHE_SIZE) {
return new OperandSubstitutionSource(operandIndex);
}
while (operandIndex >= CACHE.size()) {
CACHE.add(new OperandSubstitutionSource(CACHE.size()));
}
return CACHE.get(operandIndex);
}
private final int operandIndex;
private OperandSubstitutionSource(int operandIndex) {
this.operandIndex = operandIndex;
}
@Override
String substitute(M68KAssemblyContext context, int macroCounter, Macro macro) {
if (this.operandIndex < context.numberOfOperands) {
return context.getOperandText(this.operandIndex);
}
return "";
}
}
@Immutable
private static final class Substitution {
final int offset;
final int length;
@Nonnull
final SubstitutionSource source;
Substitution(int offset, int length, @Nonnull SubstitutionSource source) {
this.offset = offset;
this.length = length;
this.source = source;
}
}
@Immutable
private static abstract class SubstitutionSource {
/** Expands to the attribute on the macro invocation. */
@Nonnull
static final SubstitutionSource ATTRIBUTE = new SubstitutionSource() {
@Override
String substitute(M68KAssemblyContext context, int macroCounter, Macro macro) {
if (context.attribute != null) {
return context.attribute;
}
return "";
}
};
/** Expands to the last label on the macro invocation. */
@Nonnull
static final SubstitutionSource LABEL = new SubstitutionSource() {
@Override
String substitute(M68KAssemblyContext context, int macroCounter, Macro macro) {
if (context.numberOfLabels > 0) {
return context.getLabelText(context.numberOfLabels - 1);
}
return "";
}
};
/** Expands to the macro counter for the current macro invocation. */
@Nonnull
static final SubstitutionSource COUNTER = new SubstitutionSource() {
@Override
String substitute(M68KAssemblyContext context, int macroCounter, Macro macro) {
return "_" + macroCounter;
}
};
/** Expands to the number of operands on the macro invocation. */
@Nonnull
static final SubstitutionSource NARG = new SubstitutionSource() {
@Override
String substitute(M68KAssemblyContext context, int macroCounter, Macro macro) {
return Integer.toString(context.numberOfOperands);
}
};
/** Expands to the operands that are in the operand pack. */
@Nonnull
static final SubstitutionSource PACK = new SubstitutionSource() {
@Override
String substitute(M68KAssemblyContext context, int macroCounter, Macro macro) {
final StringBuilder sb = new StringBuilder();
// On the macro definition, if the operand pack is preceded by x operands and followed by y operands,
// then the pack includes all operands except the first x ones and the last y ones.
// macro.numberOfNamedOperands also counts the pack operand.
for (int i = 0; i <= context.numberOfOperands - macro.numberOfNamedOperands; i++) {
if (i > 0) {
sb.append(',');
}
sb.append(context.getOperandText(macro.packOperandIndex + i));
}
return sb.toString();
}
};
SubstitutionSource() {
}
@Nonnull
abstract String substitute(@Nonnull M68KAssemblyContext context, int macroCounter, @Nonnull Macro macro);
}
private static void addPositionalSubstitution(@Nonnull ArrayList<Substitution> substitutions, int startPosition,
int endPosition, int i) {
final SubstitutionSource source;
if (i == 0) {
source = SubstitutionSource.ATTRIBUTE;
} else {
source = OperandSubstitutionSource.get(i - 1);
}
substitutions.add(new Substitution(startPosition, endPosition - startPosition, source));
}
private static void findNamedSubstitution(@Nonnull String[] operands, int packOperandIndex, @Nonnull String name,
int startPosition, int endPosition, @Nonnull ArrayList<Substitution> substitutions) {
if ("NARG".equalsIgnoreCase(name)) {
substitutions.add(new Substitution(startPosition, endPosition - startPosition, SubstitutionSource.NARG));
return;
}
for (int i = 0; i < operands.length; i++) {
if (operands[i].equalsIgnoreCase(name)) {
final SubstitutionSource source;
if (i == packOperandIndex) {
source = SubstitutionSource.PACK;
} else if (packOperandIndex != -1 && i > packOperandIndex) {
source = OperandFromEndSubstitutionSource.get(i - operands.length);
} else {
source = OperandSubstitutionSource.get(i);
}
substitutions.add(new Substitution(startPosition, endPosition - startPosition, source));
break;
}
}
}
private static int findPackOperand(@Nonnull M68KAssemblyContext context, @Nonnull String[] operands) {
int result = -1;
for (int i = 0; i < operands.length; i++) {
if (operands[i].equals("...")) {
if (result == -1) {
result = i;
} else {
context.addMessage(new MultipleOperandPacksInMacroDefinitionErrorMessage());
break;
}
}
}
return result;
}
@Nonnull
private static ArrayList<Substitution> identifySubstitutions(@Nonnull String[] operands, int packOperandIndex,
@Nonnull SourceLocation body) {
// There are a few patterns that will get substituted in macros.
//
// The following patterns are matched anywhere in the macro body:
// - \{xyz} - if xyz is an integer, gets substituted to the nth operand (\0 is the attribute);
// - if xyz matches the name of an operand, gets substituted with the corresponding operand in a macro invocation
// - if xyz is ..., gets substituted with the operands that are included in the operand pack
// - \* gets substituted with the last label on the macro invocation line
// - \@ gets substituted with an increasing counter value, prefixed with an underscore (the counter is global to the assembly)
//
// The following patterns are matched outside of string literals only:
// - xyz if xyz is an identifier that matches the name of an operand, gets substituted with the corresponding operand
// in a macro invocation
// - \xyz if xyz is an integer, gets substituted with the nth operand (\0 is the attribute)
// - ... gets substituted with the operands that are included in the operand pack
final ArrayList<Substitution> substitutions = new ArrayList<>();
final RangedCharSequenceReader reader = new RangedCharSequenceReader(new DocumentReader(body.getFile().getText()),
body.getTextPosition(), body.getTextPosition() + body.getSourceNode().getLength());
int inString = -1;
while (!reader.atEnd()) {
final int startPosition = reader.getCurrentPosition();
int codePoint = reader.getCurrentCodePoint();
if (codePoint == '\\') {
reader.advance();
codePoint = reader.getCurrentCodePoint();
if (codePoint == '{') {
// Read until we find a '}'.
do {
reader.advance();
codePoint = reader.getCurrentCodePoint();
} while (codePoint != -1 && codePoint != '}');
if (codePoint == '}') {
reader.advance();
final int endPosition = reader.getCurrentPosition();
// Read the name starting after "\{" and ending before "}".
// Trim it because operands are always trimmed.
reader.setCurrentPosition(startPosition + 2);
final String name = reader.readSubstring(endPosition - startPosition - 3).trim();
final Integer i = tryParseInt(name);
if (i != null) {
addPositionalSubstitution(substitutions, startPosition, endPosition, i);
} else {
// Check if the text between the braces matches the name of an operand.
findNamedSubstitution(operands, packOperandIndex, name, startPosition, endPosition, substitutions);
}
}
continue;
}
if (codePoint == '*') {
reader.advance();
substitutions.add(new Substitution(startPosition, reader.getCurrentPosition() - startPosition,
SubstitutionSource.LABEL));
continue;
}
if (codePoint == '@') {
reader.advance();
substitutions.add(new Substitution(startPosition, reader.getCurrentPosition() - startPosition,
SubstitutionSource.COUNTER));
continue;
}
if (inString == -1) {
if (Syntax.isDigit(codePoint)) {
// Read until we find a non-digit.
do {
reader.advance();
} while (Syntax.isDigit(reader.getCurrentCodePoint()));
final int endPosition = reader.getCurrentPosition();
reader.setCurrentPosition(startPosition + 1);
final String name = reader.readSubstring(endPosition - startPosition - 1).trim();
final Integer i = tryParseInt(name);
if (i != null) {
addPositionalSubstitution(substitutions, startPosition, endPosition, i);
}
continue;
}
} else {
// Skip the next code point. This allows single and double quotes, in particular, to be escaped.
reader.advance();
}
} else {
if (inString == -1) {
if (codePoint == '\'' || codePoint == '"') {
inString = codePoint;
} else if (codePoint == '.') {
reader.advance();
if (reader.getCurrentCodePoint() == '.') {
reader.advance();
if (reader.getCurrentCodePoint() == '.') {
reader.advance();
if (packOperandIndex != -1) {
substitutions.add(new Substitution(startPosition, reader.getCurrentPosition() - startPosition,
SubstitutionSource.PACK));
}
}
}
continue;
} else {
boolean startsWithDigit = Syntax.isDigit(codePoint);
if (startsWithDigit || M68KParser.SYNTAX.isValidIdentifierInitialCodePoint(codePoint)) {
// Read an identifier.
do {
reader.advance();
} while (M68KParser.SYNTAX.isValidIdentifierCodePoint(reader.getCurrentCodePoint()));
if (!startsWithDigit) {
final int endPosition = reader.getCurrentPosition();
reader.setCurrentPosition(startPosition);
final String identifier = reader.readSubstring(endPosition - startPosition);
// Check if the identifier matches the name of an operand.
findNamedSubstitution(operands, packOperandIndex, identifier, startPosition, endPosition,
substitutions);
}
continue;
}
}
} else if (inString == codePoint) {
inString = -1;
}
}
reader.advance();
}
return substitutions;
}
@CheckForNull
private static Integer tryParseInt(@Nonnull String name) {
final int length = name.length();
if (length == 0) {
return null;
}
long result = 0;
for (int i = 0; i < length; i++) {
final char ch = name.charAt(i);
if (!Syntax.isDigit(ch)) {
return null;
}
result = result * 10 + (ch - '0');
// Overflow?
if (result > Integer.MAX_VALUE) {
return null;
}
}
return (int) result;
}
final int numberOfNamedOperands;
final int packOperandIndex;
@Nonnull
private final SourceLocation body;
@Nonnull
private final ArrayList<Substitution> substitutions;
@Nonnull
private final boolean hasLabelSubstitutions;
Macro(@Nonnull M68KAssemblyContext context, @Nonnull String[] operands, @Nonnull SourceLocation body) {
this.numberOfNamedOperands = operands.length;
this.packOperandIndex = findPackOperand(context, operands);
this.body = body;
this.substitutions = identifySubstitutions(operands, this.packOperandIndex, body);
boolean hasLabelSubstitutions = false;
for (Substitution substitution : this.substitutions) {
if (substitution.source == SubstitutionSource.LABEL) {
hasLabelSubstitutions = true;
break;
}
}
this.hasLabelSubstitutions = hasLabelSubstitutions;
}
@Override
void assemble(M68KAssemblyContext context) {
final MacroInstantiation macroInstantiation = this.substituteMacroOperands(context);
context.builder.enterFile(macroInstantiation, this.body.getArchitecture());
}
@Override
void defineLabels(M68KAssemblyContext context) {
if (this.hasLabelSubstitutions) {
context.defineExtraLabels();
} else {
context.defineLabels();
}
}
@Nonnull
private final MacroInstantiation substituteMacroOperands(@Nonnull M68KAssemblyContext context) {
MacroInstantiation result = new MacroInstantiation(this.body);
int correction = 0;
int macroCounter = context.builder.incrementMacroCounter();
for (Substitution substitution : this.substitutions) {
final String substitutedText = substitution.source.substitute(context, macroCounter, this);
result = result.replaceText(substitution.offset + correction, substitution.length, substitutedText);
correction += substitutedText.length() - substitution.length;
}
return result;
}
}