mirror of
https://gitlab.com/camelot/kickc.git
synced 2024-10-21 02:24:34 +00:00
Added support for macro parameters with proper pre-expansion allowing nested macro calls. Closes #169
This commit is contained in:
parent
682757b10c
commit
433fcd3dd4
@ -2,6 +2,7 @@ package dk.camelot64.kickc.model;
|
|||||||
|
|
||||||
import dk.camelot64.kickc.model.statements.Statement;
|
import dk.camelot64.kickc.model.statements.Statement;
|
||||||
import dk.camelot64.kickc.model.statements.StatementSource;
|
import dk.camelot64.kickc.model.statements.StatementSource;
|
||||||
|
import org.antlr.v4.runtime.Token;
|
||||||
|
|
||||||
/** Signals some error in the code (or compilation) */
|
/** Signals some error in the code (or compilation) */
|
||||||
public class CompileError extends RuntimeException {
|
public class CompileError extends RuntimeException {
|
||||||
@ -17,6 +18,10 @@ public class CompileError extends RuntimeException {
|
|||||||
this.source = source.toString();
|
this.source = source.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CompileError(String message, Token token) {
|
||||||
|
this(message, new StatementSource(token, token));
|
||||||
|
}
|
||||||
|
|
||||||
public CompileError(String message, Statement statement) {
|
public CompileError(String message, Statement statement) {
|
||||||
this(message, statement.getSource());
|
this(message, statement.getSource());
|
||||||
}
|
}
|
||||||
@ -28,4 +33,5 @@ public class CompileError extends RuntimeException {
|
|||||||
public String getSource() {
|
public String getSource() {
|
||||||
return source;
|
return source;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -29,9 +29,25 @@ public class CPreprocessor implements TokenSource {
|
|||||||
* The #defined macros.
|
* The #defined macros.
|
||||||
* Maps macro name to the tokens of the expansion
|
* Maps macro name to the tokens of the expansion
|
||||||
*/
|
*/
|
||||||
private Map<String, List<Token>> defines;
|
private Map<String, Macro> defines;
|
||||||
|
|
||||||
public CPreprocessor(TokenSource input, Map<String, List<Token>> defines) {
|
/** A defined macro. */
|
||||||
|
static class Macro {
|
||||||
|
/** The name of the define. */
|
||||||
|
final String name;
|
||||||
|
/** The parameters. Empty if there are no parameters. */
|
||||||
|
final List<String> parameters;
|
||||||
|
/** The body. */
|
||||||
|
final List<Token> body;
|
||||||
|
|
||||||
|
Macro(String name, List<String> parameters, List<Token> body) {
|
||||||
|
this.name = name;
|
||||||
|
this.parameters = parameters;
|
||||||
|
this.body = body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public CPreprocessor(TokenSource input, Map<String, Macro> defines) {
|
||||||
if(input instanceof CTokenSource) {
|
if(input instanceof CTokenSource) {
|
||||||
// If possible use the input directly instead of wrapping it
|
// If possible use the input directly instead of wrapping it
|
||||||
this.input = (CTokenSource) input;
|
this.input = (CTokenSource) input;
|
||||||
@ -134,48 +150,125 @@ public class CPreprocessor implements TokenSource {
|
|||||||
String macroName = nextToken(cTokenSource, KickCLexer.NAME).getText();
|
String macroName = nextToken(cTokenSource, KickCLexer.NAME).getText();
|
||||||
// Examine whether the macro has parameters
|
// Examine whether the macro has parameters
|
||||||
skipWhitespace(cTokenSource);
|
skipWhitespace(cTokenSource);
|
||||||
|
List<String> macroParameters = new ArrayList<>();
|
||||||
if(cTokenSource.peekToken().getType() == KickCLexer.PAR_BEGIN) {
|
if(cTokenSource.peekToken().getType() == KickCLexer.PAR_BEGIN) {
|
||||||
|
// Read past the '('
|
||||||
|
cTokenSource.nextToken();
|
||||||
// Macro has parameters - find parameter name list
|
// Macro has parameters - find parameter name list
|
||||||
throw new CompileError("Macros with parameters not supported!");
|
boolean commaNext = false;
|
||||||
|
while(true) {
|
||||||
|
skipWhitespace(cTokenSource);
|
||||||
|
final Token paramToken = cTokenSource.nextToken();
|
||||||
|
if(paramToken.getType() == KickCLexer.PAR_END) {
|
||||||
|
if(!commaNext && macroParameters.size() > 0)
|
||||||
|
throw new CompileError("Error! #define declared parameter list ends with COMMA.", paramToken);
|
||||||
|
// We reached the end of the parameters
|
||||||
|
break;
|
||||||
|
} else if(!commaNext && paramToken.getType() == KickCLexer.NAME) {
|
||||||
|
macroParameters.add(paramToken.getText());
|
||||||
|
// Now expect a comma
|
||||||
|
commaNext = true;
|
||||||
|
} else if(commaNext && paramToken.getType() == KickCLexer.COMMA)
|
||||||
|
// Got the comma we needed - expect a name
|
||||||
|
commaNext = false;
|
||||||
|
else
|
||||||
|
// Unexpected token
|
||||||
|
throw new CompileError("Error! #define declared parameter not a NAME.", paramToken);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
final ArrayList<Token> macroBody = readBody(cTokenSource);
|
final ArrayList<Token> macroBody = readBody(cTokenSource);
|
||||||
defines.put(macroName, macroBody);
|
defines.put(macroName, new Macro(macroName, macroParameters, macroBody));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encountered an IDENTIFIER. Attempt to expand as a macro.
|
* Encountered a NAME. Attempt to expand as a macro.
|
||||||
*
|
*
|
||||||
* @param inputToken The IDENTIFIER token
|
* @param macroNameToken The NAME token
|
||||||
* @param cTokenSource The token source usable for getting more tokens (eg. parameter values) - and for pushing the expanded body to the front for further processing.
|
* @param cTokenSource The token source usable for getting more tokens (eg. parameter values) - and for pushing the expanded body to the front for further processing.
|
||||||
* @return true if a macro was expanded. False if not.
|
* @return true if a macro was expanded. False if not.
|
||||||
*/
|
*/
|
||||||
private boolean expand(Token inputToken, CTokenSource cTokenSource) {
|
private boolean expand(Token macroNameToken, CTokenSource cTokenSource) {
|
||||||
final String macroName = inputToken.getText();
|
final String macroName = macroNameToken.getText();
|
||||||
List<Token> macroBody = defines.get(macroName);
|
Macro macro = defines.get(macroName);
|
||||||
if(macroBody != null) {
|
if(macro != null) {
|
||||||
// Check for macro recursion
|
// Check for macro recursion
|
||||||
if(inputToken instanceof ExpansionToken) {
|
if(macroNameToken instanceof ExpansionToken) {
|
||||||
if(((ExpansionToken) inputToken).getMacroNames().contains(macroName)) {
|
if(((ExpansionToken) macroNameToken).getMacroNames().contains(macroName)) {
|
||||||
// Detected macro recursion in the expansion - add directly to output and do not perform expansion!
|
// Detected macro recursion in the expansion - add directly to output and do not perform expansion!
|
||||||
macroBody = null;
|
macro = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(macroBody != null) {
|
if(macro != null) {
|
||||||
// Macro expansion is needed
|
// Handle parameters
|
||||||
List<Token> expandedBody = new ArrayList<>();
|
List<List<Token>> paramValues = new ArrayList<>();
|
||||||
for(Token bodyToken : macroBody) {
|
if(!macro.parameters.isEmpty()) {
|
||||||
final CommonToken expandedToken = new CommonToken(inputToken);
|
// Parse parameter value list
|
||||||
expandedToken.setText(bodyToken.getText());
|
{
|
||||||
expandedToken.setType(bodyToken.getType());
|
// Skip '('
|
||||||
expandedToken.setChannel(bodyToken.getChannel());
|
skipWhitespace(cTokenSource);
|
||||||
Set<String> macroNames = new HashSet<>();
|
nextToken(cTokenSource, KickCLexer.PAR_BEGIN);
|
||||||
if(inputToken instanceof ExpansionToken) {
|
// Read parameter values
|
||||||
// Transfer macro names to the new expansion
|
List<Token> paramValue = new ArrayList<>();
|
||||||
macroNames = ((ExpansionToken) inputToken).getMacroNames();
|
int nesting = 1;
|
||||||
|
while(true) {
|
||||||
|
skipWhitespace(cTokenSource);
|
||||||
|
final Token paramToken = cTokenSource.nextToken();
|
||||||
|
if(paramToken.getType() == KickCLexer.PAR_END && nesting == 1) {
|
||||||
|
// We reached the end of the parameters - add the current param unless it is an empty parameter alone in the list
|
||||||
|
if(!paramValues.isEmpty() || !paramValue.isEmpty())
|
||||||
|
paramValues.add(paramValue);
|
||||||
|
break;
|
||||||
|
} else if(paramToken.getType() == KickCLexer.COMMA && nesting == 1) {
|
||||||
|
// We have reached the next parameter value
|
||||||
|
paramValues.add(paramValue);
|
||||||
|
paramValue = new ArrayList<>();
|
||||||
|
} else {
|
||||||
|
// We are reading a parameter value - handle nesting and store it
|
||||||
|
if(paramToken.getType() == KickCLexer.PAR_BEGIN)
|
||||||
|
nesting++;
|
||||||
|
if(paramToken.getType() == KickCLexer.PAR_END)
|
||||||
|
nesting--;
|
||||||
|
paramValue.add(paramToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check parameter list length
|
||||||
|
if(macro.parameters.size() != paramValues.size()) {
|
||||||
|
throw new CompileError("Error! Wrong number of macro parameters. Expected " + macro.parameters.size() + " was " + paramValues.size(), macroNameToken);
|
||||||
|
}
|
||||||
|
// Expand parameter values
|
||||||
|
List<List<Token>> expandedParamValues = new ArrayList<>();
|
||||||
|
for(List<Token> paramTokens : paramValues) {
|
||||||
|
List<Token> expandedParamValue = new ArrayList<>();
|
||||||
|
CPreprocessor subPreprocessor = new CPreprocessor(new ListTokenSource(paramTokens), new HashMap<>(defines));
|
||||||
|
while(true) {
|
||||||
|
final Token expandedToken = subPreprocessor.nextToken();
|
||||||
|
if(expandedToken.getType() == Token.EOF)
|
||||||
|
break;
|
||||||
|
else
|
||||||
|
expandedParamValue.add(expandedToken);
|
||||||
|
}
|
||||||
|
expandedParamValues.add(expandedParamValue);
|
||||||
|
paramValues = expandedParamValues;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform macro expansion - by expanding the body at the start of the token source
|
||||||
|
List<Token> expandedBody = new ArrayList<>();
|
||||||
|
final List<Token> macroBody = macro.body;
|
||||||
|
for(Token macroBodyToken : macroBody) {
|
||||||
|
if(macroBodyToken.getType()==KickCLexer.NAME && macro.parameters.contains(macroBodyToken.getText())) {
|
||||||
|
// body token is a parameter name - replace with expanded parameter value
|
||||||
|
final int paramIndex = macro.parameters.indexOf(macroBodyToken.getText());
|
||||||
|
final List<Token> expandedParamValue = paramValues.get(paramIndex);
|
||||||
|
for(Token expandedParamValueToken : expandedParamValue) {
|
||||||
|
addTokenToExpandedBody(expandedParamValueToken, macroNameToken, expandedBody);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// body token is a normal token
|
||||||
|
addTokenToExpandedBody(macroBodyToken, macroNameToken, expandedBody);
|
||||||
}
|
}
|
||||||
macroNames.add(macroName);
|
|
||||||
expandedBody.add(new ExpansionToken(expandedToken, macroNames));
|
|
||||||
}
|
}
|
||||||
cTokenSource.addSource(new ListTokenSource(expandedBody));
|
cTokenSource.addSource(new ListTokenSource(expandedBody));
|
||||||
return true;
|
return true;
|
||||||
@ -183,6 +276,26 @@ public class CPreprocessor implements TokenSource {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a macro token to the exapnded macro body. Keeps track of which macros has been used to expand the token using {@link ExpansionToken}
|
||||||
|
* @param macroBodyToken The macro body token to add
|
||||||
|
* @param macroNameToken The token containing the macro name. Used to get the name and as a source for copying token properties (ensuring file name, line etc. are OK).
|
||||||
|
* @param expandedBody The expanded macro body to add the token to
|
||||||
|
*/
|
||||||
|
private void addTokenToExpandedBody(Token macroBodyToken, Token macroNameToken, List<Token> expandedBody) {
|
||||||
|
final CommonToken expandedToken = new CommonToken(macroNameToken);
|
||||||
|
expandedToken.setText(macroBodyToken.getText());
|
||||||
|
expandedToken.setType(macroBodyToken.getType());
|
||||||
|
expandedToken.setChannel(macroBodyToken.getChannel());
|
||||||
|
Set<String> macroNames = new HashSet<>();
|
||||||
|
if(macroNameToken instanceof ExpansionToken) {
|
||||||
|
// Transfer macro names to the new expansion
|
||||||
|
macroNames = ((ExpansionToken) macroNameToken).getMacroNames();
|
||||||
|
}
|
||||||
|
macroNames.add(macroNameToken.getText());
|
||||||
|
expandedBody.add(new ExpansionToken(expandedToken, macroNames));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Undefine a macro.
|
* Undefine a macro.
|
||||||
*
|
*
|
||||||
@ -255,7 +368,8 @@ public class CPreprocessor implements TokenSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Evaluate a constant condition expression from #if. The special defined operator must be evaluated before calling this.
|
* Evaluate a constant condition expression from #if / #eilf.
|
||||||
|
* The special defined operator must be evaluated before calling this.
|
||||||
*
|
*
|
||||||
* @param conditionExpr The expression
|
* @param conditionExpr The expression
|
||||||
* @return The result of the evaluation.
|
* @return The result of the evaluation.
|
||||||
@ -338,7 +452,7 @@ public class CPreprocessor implements TokenSource {
|
|||||||
hasPar = true;
|
hasPar = true;
|
||||||
}
|
}
|
||||||
if(token.getType() != KickCLexer.NAME) {
|
if(token.getType() != KickCLexer.NAME) {
|
||||||
throw new CompileError("Unexpected token. Was expecting NAME!");
|
throw new CompileError("Unexpected token. Was expecting NAME!", token);
|
||||||
}
|
}
|
||||||
tokenIt.remove();
|
tokenIt.remove();
|
||||||
Token macroNameToken = token;
|
Token macroNameToken = token;
|
||||||
@ -347,7 +461,7 @@ public class CPreprocessor implements TokenSource {
|
|||||||
// Skip closing parenthesis
|
// Skip closing parenthesis
|
||||||
token = getNextSkipWhitespace(tokenIt);
|
token = getNextSkipWhitespace(tokenIt);
|
||||||
if(token.getType() != KickCLexer.PAR_END) {
|
if(token.getType() != KickCLexer.PAR_END) {
|
||||||
throw new CompileError("Unexpected token. Was expecting ')'!");
|
throw new CompileError("Unexpected token. Was expecting ')'!", token);
|
||||||
}
|
}
|
||||||
tokenIt.remove();
|
tokenIt.remove();
|
||||||
}
|
}
|
||||||
@ -470,7 +584,7 @@ public class CPreprocessor implements TokenSource {
|
|||||||
private Token nextToken(CTokenSource cTokenSource, int tokenType) {
|
private Token nextToken(CTokenSource cTokenSource, int tokenType) {
|
||||||
final Token token = cTokenSource.nextToken();
|
final Token token = cTokenSource.nextToken();
|
||||||
if(token.getType() != tokenType)
|
if(token.getType() != tokenType)
|
||||||
throw new CompileError("Unexpected token. Was expecting " + tokenType);
|
throw new CompileError("Unexpected token. Was expecting " + tokenType, token);
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package dk.camelot64.kickc.parsing.macros;
|
package dk.camelot64.kickc.parsing.macros;
|
||||||
|
|
||||||
|
import dk.camelot64.kickc.model.CompileError;
|
||||||
import dk.camelot64.kickc.parser.CParser;
|
import dk.camelot64.kickc.parser.CParser;
|
||||||
import dk.camelot64.kickc.parser.KickCParser;
|
import dk.camelot64.kickc.parser.KickCParser;
|
||||||
import dk.camelot64.kickc.parser.KickCParserBaseVisitor;
|
import dk.camelot64.kickc.parser.KickCParserBaseVisitor;
|
||||||
@ -7,7 +8,12 @@ import org.antlr.v4.runtime.CharStreams;
|
|||||||
import org.antlr.v4.runtime.CodePointCharStream;
|
import org.antlr.v4.runtime.CodePointCharStream;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
|
||||||
|
import static junit.framework.TestCase.fail;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test the C preprocessor
|
* Test the C preprocessor
|
||||||
@ -163,14 +169,53 @@ public class TestPreprocessor {
|
|||||||
assertEquals("name:y;name:w;", parse("#if 0\nx;\n#elif 1\ny;\n#elif 1\nz;\n#else\nq;\n#endif\nw;"));
|
assertEquals("name:y;name:w;", parse("#if 0\nx;\n#elif 1\ny;\n#elif 1\nz;\n#else\nq;\n#endif\nw;"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test #define with wrong syntax
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testErrors() {
|
||||||
|
// Declared parameters are not names
|
||||||
|
assertError("#define f(x,1) x", "Error! #define declared parameter not a NAME.", true);
|
||||||
|
// Declared parameter list ends with comma
|
||||||
|
assertError("#define f(x,y,) x", "#define declared parameter list ends with COMMA.", true);
|
||||||
|
// Number of parameters not matching
|
||||||
|
assertError("#define f(x,y) x+y\nf(7);", "Error! Wrong number of macro parameters. Expected 2 was 1", true);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test define with parameters
|
* Test define with parameters
|
||||||
*/
|
*/
|
||||||
//@Test
|
@Test
|
||||||
//public void testDefineParams() {
|
public void testDefineParams() {
|
||||||
// // A simple unused define
|
// A simple define with one parameter
|
||||||
// assertEquals("+(name:b,num:1);*(name:c,num:2);", parse("#define A(a) a+1\nA(b)"));
|
assertEquals("+(name:b,num:1);", parse("#define A(a) a+1\nA(b);"));
|
||||||
//}
|
// A simple define with one parameter used twice
|
||||||
|
assertEquals("+(name:b,num:1);+(name:c,num:1);", parse("#define A(a) a+1\nA(b);A(c);"));
|
||||||
|
// A simple define with two parameters
|
||||||
|
assertEquals("+(num:1,name:x);", parse("#define A(a,b) a+b\nA(1,x);"));
|
||||||
|
// A nested call
|
||||||
|
assertEquals("+(+(name:x,num:1),num:1);", parse("#define A(a) a+1\nA(A(x));"));
|
||||||
|
// A double nested call
|
||||||
|
assertEquals("+(+(+(name:x,num:1),num:1),num:1);", parse("#define A(a) a+1\nA(A(A(x)));"));
|
||||||
|
// A nested call for a 2-parameter macro
|
||||||
|
assertEquals("+(+(+(name:x,name:y),name:z),name:w);", parse("#define A(a,b) a+b\nA(A(x,y),A(z,w));"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertError(String program, String expectError, boolean expectLineNumber) {
|
||||||
|
try {
|
||||||
|
parse(program);
|
||||||
|
} catch(CompileError e) {
|
||||||
|
System.out.println("Got error: " + e.getMessage());
|
||||||
|
// expecting error!
|
||||||
|
assertTrue("Error message expected '" + expectError + "' - was:" + e.getMessage(), e.getMessage().contains(expectError));
|
||||||
|
if(expectLineNumber) {
|
||||||
|
// expecting line number!
|
||||||
|
assertTrue("Error message expected line number - was:" + e.getMessage(), e.getMessage().contains("Line"));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fail("Expected compile error.");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a program with macros and return the resulting syntax tree
|
* Parse a program with macros and return the resulting syntax tree
|
||||||
|
Loading…
Reference in New Issue
Block a user