Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for parsing enums #606

Merged
merged 7 commits into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ The **"Breaking Changes"** listed below are changes that have been made in the d
- Add a `Close Project` option
- Add charts to the `Wild Pokémon` tab that show species and level distributions.
- An alert will be displayed when attempting to open a seemingly invalid project.
- Add support for defining project values with `enum` where `#define` was expected.

### Changed
- Edits to map connections now have Undo/Redo and can be viewed in exported timelapses.
Expand Down Expand Up @@ -42,6 +43,7 @@ The **"Breaking Changes"** listed below are changes that have been made in the d
- Fix map connections rendering incorrectly if their dimensions were smaller than the border draw distance.
- Fix the map list filter retaining text between project open/close.
- Fix the map list mishandling value gaps when sorting by Area.
- Fix a freeze on startup if project values are defined with mismatched parentheses.

## [5.4.1] - 2024-03-21
### Fixed
Expand Down
16 changes: 11 additions & 5 deletions include/core/parseutil.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ class ParseUtil
QString readCIncbin(const QString &text, const QString &label);
QMap<QString, QString> readCIncbinMulti(const QString &filepath);
QStringList readCIncbinArray(const QString &filename, const QString &label);
QMap<QString, int> readCDefinesByPrefix(const QString &filename, QStringList prefixes);
QMap<QString, int> readCDefinesByName(const QString &filename, QStringList names);
QStringList readCDefineNames(const QString&, const QStringList&);
QMap<QString, int> readCDefinesByRegex(const QString &filename, const QStringList &regexList);
QMap<QString, int> readCDefinesByName(const QString &filename, const QStringList &names);
QStringList readCDefineNames(const QString &filename, const QStringList &regexList);
QMap<QString, QHash<QString, QString>> readCStructs(const QString &, const QString & = "", const QHash<int, QString> = { });
QList<QStringList> getLabelMacros(const QList<QStringList>&, const QString&);
QStringList getLabelValues(const QList<QStringList>&, const QString&);
Expand Down Expand Up @@ -97,8 +97,14 @@ class ParseUtil
void recordErrors(const QStringList &errors);
void logRecordedErrors();
QString createErrorMessage(const QString &message, const QString &expression);
QString readCDefinesFile(const QString &filename);
QMap<QString, int> readCDefines(const QString &filename, const QStringList &searchText, bool fullMatch);

struct ParsedDefines {
QMap<QString,QString> expressions; // Map of all define names encountered to their expressions
QStringList filteredNames; // List of define names that matched the search text, in the order that they were encountered
};
ParsedDefines readCDefines(const QString &filename, const QStringList &filterList, bool useRegex);
QMap<QString, int> evaluateCDefines(const QString &filename, const QStringList &filterList, bool useRegex);
bool defineNameMatchesFilter(const QString &name, const QStringList &filterList, bool useRegex);

static const QRegularExpression re_incScriptLabel;
static const QRegularExpression re_globalIncScriptLabel;
Expand Down
162 changes: 95 additions & 67 deletions src/core/parseutil.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,22 @@ const QRegularExpression ParseUtil::re_poryScriptLabel("\\b(script)(\\((global|l
const QRegularExpression ParseUtil::re_globalPoryScriptLabel("\\b(script)(\\((global)\\))?\\s*\\b(?<label>[\\w_][\\w\\d_]*)");
const QRegularExpression ParseUtil::re_poryRawSection("\\b(raw)\\s*`(?<raw_script>[^`]*)");

static const QMap<QString, int> globalDefineValues = {
{"FALSE", 0},
{"TRUE", 1},
{"SCHAR_MIN", SCHAR_MIN},
{"SCHAR_MAX", SCHAR_MAX},
{"CHAR_MIN", CHAR_MIN},
{"CHAR_MAX", CHAR_MAX},
{"UCHAR_MAX", UCHAR_MAX},
{"SHRT_MIN", SHRT_MIN},
{"SHRT_MAX", SHRT_MAX},
{"USHRT_MAX", USHRT_MAX},
{"INT_MIN", INT_MIN},
{"INT_MAX", INT_MAX},
{"UINT_MAX", UINT_MAX},
};

using OrderedJson = poryjson::Json;

ParseUtil::ParseUtil() { }
Expand Down Expand Up @@ -225,10 +241,11 @@ QList<Token> ParseUtil::generatePostfix(const QList<Token> &tokens) {
}

while (!operatorStack.isEmpty()) {
if (operatorStack.top().value == "(" || operatorStack.top().value == ")") {
Token token = operatorStack.pop();
if (token.value == "(" || token.value == ")") {
recordError("Mismatched parentheses detected in expression!");
} else {
output.append(operatorStack.pop());
output.append(token);
}
}

Expand Down Expand Up @@ -353,111 +370,122 @@ QStringList ParseUtil::readCIncbinArray(const QString &filename, const QString &
return paths;
}

QString ParseUtil::readCDefinesFile(const QString &filename)
{
bool ParseUtil::defineNameMatchesFilter(const QString &name, const QStringList &filterList, bool useRegex) {
for (auto filter : filterList) {
if (useRegex) {
// TODO: These QRegularExpression should probably be constructed beforehand,
// otherwise we recreate them for every define we check.
if (QRegularExpression(filter).match(name).hasMatch()) return true;
} else if (name == filter) return true;
}
return false;
}

ParseUtil::ParsedDefines ParseUtil::readCDefines(const QString &filename, const QStringList &filterList, bool useRegex) {
ParsedDefines result;
this->file = filename;

if (this->file.isEmpty()) {
return QString();
return result;
}

QString filepath = this->root + "/" + this->file;
this->text = readTextFile(filepath);

if (this->text.isNull()) {
logError(QString("Failed to read C defines file: '%1'").arg(filepath));
return QString();
return result;
}

static const QRegularExpression re_extraChars("(//.*)|(\\/+\\*+[^*]*\\*+\\/+)");
this->text.replace(re_extraChars, "");
static const QRegularExpression re_extraSpaces("(\\\\\\s+)");
this->text.replace(re_extraSpaces, "");
return this->text;
}

// Read all the define names and their expressions in the specified file, then evaluate the ones matching the search text (and any they depend on).
// If 'fullMatch' is true, 'searchText' is a list of exact define names to evaluate and return.
// If 'fullMatch' is false, 'searchText' is a list of prefixes or regexes for define names to evaluate and return.
QMap<QString, int> ParseUtil::readCDefines(const QString &filename, const QStringList &searchText, bool fullMatch)
{
QMap<QString, int> filteredValues;
if (this->text.isEmpty())
return result;

this->text = this->readCDefinesFile(filename);
if (this->text.isEmpty()) {
return filteredValues;
}
// Capture either the name and value of a #define, or everything between the braces of 'enum { }'
static const QRegularExpression re("#define\\s+(?<defineName>\\w+)[\\s\\n][^\\S\\n]*(?<defineValue>.+)?"
"|\\benum\\b[^{]*{(?<enumBody>[^}]*)}");

// Extract all the define names and expressions
QMap<QString, QString> allExpressions;
QMap<QString, QString> filteredExpressions;
static const QRegularExpression re("#define\\s+(?<defineName>\\w+)[^\\S\\n]+(?<defineValue>.+)");
QRegularExpressionMatchIterator iter = re.globalMatch(this->text);
while (iter.hasNext()) {
QRegularExpressionMatch match = iter.next();
const QString name = match.captured("defineName");
const QString expression = match.captured("defineValue");
// If name matches the search text record it for evaluation.
for (auto s : searchText) {
if ((fullMatch && name == s) || (!fullMatch && (name.startsWith(s) || QRegularExpression(s).match(name).hasMatch()))) {
filteredExpressions.insert(name, expression);
break;
const QString enumBody = match.captured("enumBody");
if (!enumBody.isNull()) {
// Encountered an enum, extract the elements of the enum and give each an appropriate expression
int baseNum = 0;
QString baseExpression = "0";

// Note: We lazily consider an enum's expression to be any characters after the assignment up until the first comma or EOL.
// This would be a problem for e.g. NAME = MACRO(a, b), but we're currently unable to parse function-like macros anyway.
// If this changes then the regex below needs to be updated.
static const QRegularExpression re_enumElement("\\b(?<name>\\w+)\\b\\s*=?\\s*(?<expression>[^,]*)");
QRegularExpressionMatchIterator elementIter = re_enumElement.globalMatch(enumBody);
while (elementIter.hasNext()) {
QRegularExpressionMatch elementMatch = elementIter.next();
const QString name = elementMatch.captured("name");
QString expression = elementMatch.captured("expression");
if (expression.isEmpty()) {
// enum values may use tokens that we don't know how to evaluate yet.
// For now we define each element to be 1 + the previous element's expression.
expression = QString("((%1)+%2)").arg(baseExpression).arg(baseNum++);
} else {
// This element was explicitly assigned an expression with '=', reset the bases for any subsequent elements.
baseExpression = expression;
baseNum = 1;
}
result.expressions.insert(name, expression);
if (defineNameMatchesFilter(name, filterList, useRegex))
result.filteredNames.append(name);
}
} else {
// Encountered a #define
const QString name = match.captured("defineName");
result.expressions.insert(name, match.captured("defineValue"));
if (defineNameMatchesFilter(name, filterList, useRegex))
result.filteredNames.append(name);
}
allExpressions.insert(name, expression);
}
return result;
}

QMap<QString, int> allValues;
allValues.insert("FALSE", 0);
allValues.insert("TRUE", 1);
// Read all the define names and their expressions in the specified file, then evaluate the ones matching the search text (and any they depend on).
QMap<QString, int> ParseUtil::evaluateCDefines(const QString &filename, const QStringList &filterList, bool useRegex) {
ParsedDefines defines = readCDefines(filename, filterList, useRegex);

// Evaluate defines
QMap<QString, int> filteredValues;
QMap<QString, int> allValues = globalDefineValues;
this->errorMap.clear();
while (!filteredExpressions.isEmpty()) {
const QString name = filteredExpressions.firstKey();
const QString expression = filteredExpressions.take(name);
while (!defines.filteredNames.isEmpty()) {
const QString name = defines.filteredNames.takeFirst();
const QString expression = defines.expressions.take(name);
if (expression == " ") continue;
this->curDefine = name;
filteredValues.insert(name, evaluateDefine(name, expression, &allValues, &allExpressions));
filteredValues.insert(name, evaluateDefine(name, expression, &allValues, &defines.expressions));
logRecordedErrors(); // Only log errors for defines that Porymap is looking for
}
return filteredValues;
}

// Find and evaluate an unknown list of defines with a known name prefix.
QMap<QString, int> ParseUtil::readCDefinesByPrefix(const QString &filename, QStringList prefixes) {
prefixes.removeDuplicates();
return this->readCDefines(filename, prefixes, false);
return filteredValues;
}

// Find and evaluate a specific set of defines with known names.
QMap<QString, int> ParseUtil::readCDefinesByName(const QString &filename, QStringList names) {
names.removeDuplicates();
return this->readCDefines(filename, names, true);
QMap<QString, int> ParseUtil::readCDefinesByName(const QString &filename, const QStringList &names) {
return evaluateCDefines(filename, names, false);
}

// Similar to readCDefines, but for cases where we only need to show a list of define names.
// We can skip reading/evaluating any expressions (and by extension skip reporting any errors from this process).
QStringList ParseUtil::readCDefineNames(const QString &filename, const QStringList &prefixes) {
QStringList filteredNames;

this->text = this->readCDefinesFile(filename);
if (this->text.isEmpty()) {
return filteredNames;
}
// Find and evaluate an unknown list of defines with a known name pattern.
QMap<QString, int> ParseUtil::readCDefinesByRegex(const QString &filename, const QStringList &regexList) {
return evaluateCDefines(filename, regexList, true);
}

static const QRegularExpression re("#define\\s+(?<defineName>\\w+)[^\\S\\n]+");
QRegularExpressionMatchIterator iter = re.globalMatch(this->text);
while (iter.hasNext()) {
QRegularExpressionMatch match = iter.next();
QString name = match.captured("defineName");
for (QString prefix : prefixes) {
if (name.startsWith(prefix) || QRegularExpression(prefix).match(name).hasMatch()) {
filteredNames.append(name);
}
}
}
return filteredNames;
// Find an unknown list of defines with a known name pattern.
// Similar to readCDefinesByRegex, but for cases where we only need to show a list of define names.
// We can skip evaluating any expressions (and by extension skip reporting any errors from this process).
QStringList ParseUtil::readCDefineNames(const QString &filename, const QStringList &regexList) {
return readCDefines(filename, regexList, true).filteredNames;
}

QStringList ParseUtil::readCArray(const QString &filename, const QString &label) {
Expand Down
Loading
Loading