Skip to content

Commit

Permalink
Process macro params before substituting new macros (#187)
Browse files Browse the repository at this point in the history
- closes #186
- refactor macro processing code to be more readable
- add unit test
- changelog update
  • Loading branch information
jlchmura authored Jan 21, 2025
1 parent b901145 commit b9115b4
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 83 deletions.
175 changes: 92 additions & 83 deletions server/src/compiler/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -703,94 +703,103 @@ export namespace LpcParser {
const tokenValue = scanner.getTokenValue();
let macro = macroTable.get(tokenValue);

if (macro && macro.disabled !== true && macro.range) {
// we are in a macro substitution
macro.disabled = true;
macro.originFilename = scanner.getFileName();
macro.posInOrigin = scanner.getTokenFullStart();
macro.endInOrigin = scanner.getTokenFullStart() + tokenValue.length + 1;
macro.pos = getPositionState();
macro.end = macro.pos.tokenStart + tokenValue.length;

const { range, arguments: macroArgsDef } = macro;

// check if this macro has argumnets
const argValsByName: MapLike<MacroParameter> = {};
if (macroArgsDef?.length > 0) {
// scan the next token, which should be an open paren.. parseArg wil check it.
currentToken = nextTokenWithoutCheck();

const values = parseMacroArguments();
const valuesEndPos = scanner.getTokenFullStart();

// now organize macro params by name
let lastArg = lastOrUndefined(values);
forEach(macroArgsDef, (arg, i) => {
Debug.assertIsDefined(arg.name);
const argName = (arg.name as Identifier).text;
if (values.length > i) {
const argVal = values[i];
argValsByName[argName] = argVal;
} else {
parseErrorAt(lastArg?.end ?? valuesEndPos, valuesEndPos, scanner.getFileName(), Diagnostics.Missing_argument_for_macro_0, argName);
argValsByName[argName] = {
posInOrigin: lastArg?.end ?? valuesEndPos,
endInOrigin: lastArg?.end ?? valuesEndPos,
pos: 0,
end: 0,
text: "",
disabled: false,
}
}
});
// check macro params first - even if tokenValue matches a macro, we should not substitute if it is a macro param
if (currentMacro && currentMacro.argsIn?.[tokenValue] && currentMacro.argsIn?.[tokenValue].disabled !== true) {
return processMacroParamSubstitution(incomingToken, tokenValue, currentMacro);
} else if (macro && macro.disabled !== true && macro.range) {
return processMacroSubstitution(incomingToken, tokenValue, macro);
}
}

return currentToken = incomingToken;
}

macro.argsIn = argValsByName;
}

// create a scanner for this macro
const saveCurrentMacro = currentMacro;

scanner.switchStream(
macro.includeFilename ?? macro.originFilename, macro.getText(), range.pos, range.end - range.pos, true /* revertOnEOF */,
() => {
// re-enable the macro
Debug.assertIsDefined(macro);
macro.disabled = false;
macro.argsIn = undefined;
macro.originFilename = undefined!;
macro.posInOrigin = undefined!;
macro.endInOrigin = undefined!;
macro.pos = undefined!;
currentMacro = saveCurrentMacro!;

// parse args will have consumed the next token, so we need to store that and return it
// when the previous scanner gets restored
return macroArgsDef?.length > 0;
}
);

currentMacro = macro;

// scan again using the new scanner
return nextTokenWithoutCheck();
} else if (currentMacro && currentMacro.argsIn?.[tokenValue] && currentMacro.argsIn?.[tokenValue].disabled !== true) {
// this is a macro parameter
const arg = currentMacro.argsIn[tokenValue];
arg.disabled = true;

scanner.switchStream(
"", arg.text, arg.pos, arg.end, true /* revertOnEOF */,
() => {
arg.disabled = false;
return false;
function processMacroParamSubstitution(incomingToken: SyntaxKind, tokenValue: string, macro: Macro): SyntaxKind {
// this is a macro parameter
const arg = macro.argsIn[tokenValue];
arg.disabled = true;

scanner.switchStream(
"", arg.text, arg.pos, arg.end, true /* revertOnEOF */,
() => {
arg.disabled = false;
return false;
}
);

return nextTokenWithoutCheck();// currentToken = scanner.scan();
}

function processMacroSubstitution(incomingToken: SyntaxKind, tokenValue: string, macro: Macro): SyntaxKind {
// we are in a macro substitution
macro.disabled = true;
macro.originFilename = scanner.getFileName();
macro.posInOrigin = scanner.getTokenFullStart();
macro.endInOrigin = scanner.getTokenFullStart() + tokenValue.length + 1;
macro.pos = getPositionState();
macro.end = macro.pos.tokenStart + tokenValue.length;

const { range, arguments: macroArgsDef } = macro;

// check if this macro has argumnets
const argValsByName: MapLike<MacroParameter> = {};
if (macroArgsDef?.length > 0) {
// scan the next token, which should be an open paren.. parseArg wil check it.
currentToken = nextTokenWithoutCheck();

const values = parseMacroArguments();
const valuesEndPos = scanner.getTokenFullStart();

// now organize macro params by name
let lastArg = lastOrUndefined(values);
forEach(macroArgsDef, (arg, i) => {
Debug.assertIsDefined(arg.name);
const argName = (arg.name as Identifier).text;
if (values.length > i) {
const argVal = values[i];
argValsByName[argName] = argVal;
} else {
parseErrorAt(lastArg?.end ?? valuesEndPos, valuesEndPos, scanner.getFileName(), Diagnostics.Missing_argument_for_macro_0, argName);
argValsByName[argName] = {
posInOrigin: lastArg?.end ?? valuesEndPos,
endInOrigin: lastArg?.end ?? valuesEndPos,
pos: 0,
end: 0,
text: "",
disabled: false,
}
);
}
});

return nextTokenWithoutCheck();// currentToken = scanner.scan();
}
macro.argsIn = argValsByName;
}

return currentToken = incomingToken;
// create a scanner for this macro
const saveCurrentMacro = currentMacro;

scanner.switchStream(
macro.includeFilename ?? macro.originFilename, macro.getText(), range.pos, range.end - range.pos, true /* revertOnEOF */,
() => {
// re-enable the macro
Debug.assertIsDefined(macro);
macro.disabled = false;
macro.argsIn = undefined;
macro.originFilename = undefined!;
macro.posInOrigin = undefined!;
macro.endInOrigin = undefined!;
macro.pos = undefined!;
currentMacro = saveCurrentMacro!;

// parse args will have consumed the next token, so we need to store that and return it
// when the previous scanner gets restored
return macroArgsDef?.length > 0;
}
);

currentMacro = macro;

// scan again using the new scanner
return nextTokenWithoutCheck();
}

function nextTokenAnd<T>(func: () => T): T {
Expand Down
2 changes: 2 additions & 0 deletions server/src/tests/__snapshots__/compiler.spec.ts.snap

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions server/src/tests/cases/compiler/macro3.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#define y add+x;
#define member(x, y) fn(y, x)

void fn(mixed a, mixed b) {}

test() {
string *weaptype = ({});
member(weaptype, "sword");
}

/**
* This tests issue 186: https://github.com/jlchmura/lpc-language-server/issues/186
* The macro param `y` should not get substituted by the `add+x` macro.
*/

0 comments on commit b9115b4

Please sign in to comment.