hachem.wtf
aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorhachem <im@hachem.wtf>2025-12-14 16:44:22 +0100
committerhachem <im@hachem.wtf>2025-12-14 16:44:22 +0100
commit0e8a995bc2cb5416228c12add93d10a631b7f952 (patch)
tree0c440a2a7c537df5f6bad73d901638d59fdd0470 /src
parent643287696ea9baccc90a26643b6adb9fabcdf433 (diff)
feat: macro expansion
Diffstat (limited to 'src')
-rw-r--r--src/ui/macro_expander.cpp802
-rw-r--r--src/ui/macro_expander.h76
-rw-r--r--src/ui/source_view.cpp132
-rw-r--r--src/ui/source_view.h17
4 files changed, 1021 insertions, 6 deletions
diff --git a/src/ui/macro_expander.cpp b/src/ui/macro_expander.cpp
new file mode 100644
index 0000000..5c53482
--- /dev/null
+++ b/src/ui/macro_expander.cpp
@@ -0,0 +1,802 @@
+#include "ui/macro_expander.h"
+
+#include <algorithm>
+#include <cctype>
+#include <fstream>
+
+namespace Hsdbg
+{
+ namespace
+ {
+ constexpr size_t MAX_TOKENS = 200000;
+ constexpr int MAX_INCLUDE_DEPTH = 8;
+
+ auto is_ident_start(char c) -> bool
+ {
+ return std::isalpha(static_cast<unsigned char>(c)) != 0 || c == '_';
+ }
+
+ auto is_ident(char c) -> bool
+ {
+ return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == '_';
+ }
+
+ auto lex_one(std::string_view text, size_t& at) -> PpToken
+ {
+ while (at < text.size() && std::isspace(static_cast<unsigned char>(text[at])) != 0)
+ ++at;
+
+ PpToken token;
+
+ if (at >= text.size())
+ return token;
+
+ const char c = text[at];
+
+ if (is_ident_start(c))
+ {
+ const size_t start = at;
+ while (at < text.size() && is_ident(text[at]))
+ ++at;
+
+ token.kind = PpKind::Identifier;
+ token.text = std::string(text.substr(start, at - start));
+ return token;
+ }
+
+ if (std::isdigit(static_cast<unsigned char>(c)) != 0 ||
+ (c == '.' && at + 1 < text.size() &&
+ std::isdigit(static_cast<unsigned char>(text[at + 1])) != 0))
+ {
+ const size_t start = at++;
+
+ while (at < text.size())
+ {
+ const char d = text[at];
+
+ if ((d == 'e' || d == 'E' || d == 'p' || d == 'P') && at + 1 < text.size() &&
+ (text[at + 1] == '+' || text[at + 1] == '-'))
+ {
+ at += 2;
+ continue;
+ }
+
+ if (is_ident(d) || d == '.')
+ ++at;
+ else
+ break;
+ }
+
+ token.kind = PpKind::Number;
+ token.text = std::string(text.substr(start, at - start));
+ return token;
+ }
+
+ if (c == '"' || c == '\'')
+ {
+ const size_t start = at++;
+
+ while (at < text.size())
+ {
+ if (text[at] == '\\' && at + 1 < text.size())
+ {
+ at += 2;
+ continue;
+ }
+
+ if (text[at] == c)
+ {
+ ++at;
+ break;
+ }
+
+ ++at;
+ }
+
+ token.kind = c == '"' ? PpKind::String : PpKind::Char;
+ token.text = std::string(text.substr(start, at - start));
+ return token;
+ }
+
+ static constexpr std::string_view multis[] = {
+ "...", "<<=", ">>=", "->*", "::", "->", "##", "<<", ">>", "<=", ">=",
+ "==", "!=", "&&", "||", "+=", "-=", "*=", "/=", "%=", "&=", "|=",
+ "^=", "++", "--", ".*",
+ };
+
+ for (const std::string_view op : multis)
+ {
+ if (text.compare(at, op.size(), op) == 0)
+ {
+ at += op.size();
+ token.kind = PpKind::Punct;
+ token.text = std::string(op);
+ return token;
+ }
+ }
+
+ token.kind = PpKind::Punct;
+ token.text = std::string(1, c);
+ ++at;
+ return token;
+ }
+
+ auto tokenize(std::string_view text) -> std::vector<PpToken>
+ {
+ std::vector<PpToken> tokens;
+ size_t at = 0;
+
+ while (at < text.size() && tokens.size() < MAX_TOKENS)
+ {
+ PpToken token = lex_one(text, at);
+
+ if (token.text.empty())
+ break;
+
+ tokens.push_back(std::move(token));
+ }
+
+ return tokens;
+ }
+
+ auto hidden_by(const PpToken& token, const std::string& name) -> bool
+ {
+ return std::ranges::find(token.hide, name) != token.hide.end();
+ }
+
+ auto needs_space(const PpToken& left, const PpToken& right) -> bool
+ {
+ const std::string& l = left.text;
+ const std::string& r = right.text;
+
+ if (r == ")" || r == "]" || r == "," || r == ";" || r == "::" ||
+ r == "." || r == "->")
+ return false;
+
+ if (l == "(" || l == "[" || l == "::" || l == "." || l == "->")
+ return false;
+
+ if (r == "(" && (left.kind == PpKind::Identifier || l == ")" || l == "]"))
+ return false;
+
+ return true;
+ }
+
+ auto render(const std::vector<PpToken>& tokens) -> std::string
+ {
+ std::string out;
+
+ for (size_t i = 0; i < tokens.size(); ++i)
+ {
+ if (i != 0 && needs_space(tokens[i - 1], tokens[i]))
+ out += ' ';
+
+ out += tokens[i].text;
+ }
+
+ return out;
+ }
+
+ auto param_index(const MacroDef& def, const std::string& name) -> int
+ {
+ for (size_t i = 0; i < def.params.size(); ++i)
+ {
+ if (def.params[i] == name)
+ return static_cast<int>(i);
+ }
+
+ return -1;
+ }
+
+ auto stringize(const std::vector<PpToken>& arg) -> PpToken
+ {
+ std::string inner = render(arg);
+ std::string escaped;
+
+ for (const char c : inner)
+ {
+ if (c == '\\' || c == '"')
+ escaped += '\\';
+
+ escaped += c;
+ }
+
+ return PpToken{ PpKind::String, "\"" + escaped + "\"", {} };
+ }
+
+ auto paste_token(const std::string& text) -> PpToken
+ {
+ std::vector<PpToken> lexed = tokenize(text);
+
+ if (lexed.size() == 1)
+ return lexed.front();
+
+ return PpToken{ PpKind::Identifier, text, {} };
+ }
+
+ using ArgList = std::vector<std::vector<PpToken>>;
+
+ auto expand_full(const MacroTable& table, std::vector<PpToken> tokens)
+ -> std::vector<PpToken>;
+
+ auto substitute(const MacroDef& def, const ArgList& raw, const ArgList& expanded)
+ -> std::vector<PpToken>
+ {
+ std::vector<PpToken> out;
+
+ const auto raw_tokens = [&](int index) -> std::vector<PpToken>
+ {
+ if (index >= 0 && index < static_cast<int>(raw.size()))
+ return raw[static_cast<size_t>(index)];
+
+ return {};
+ };
+
+ const auto expanded_tokens = [&](int index) -> std::vector<PpToken>
+ {
+ if (index >= 0 && index < static_cast<int>(expanded.size()))
+ return expanded[static_cast<size_t>(index)];
+
+ return {};
+ };
+
+ const auto raw_varargs = [&]() -> std::vector<PpToken>
+ {
+ std::vector<PpToken> joined;
+
+ for (size_t i = def.params.size(); i < raw.size(); ++i)
+ {
+ if (i != def.params.size())
+ joined.push_back(PpToken{ PpKind::Punct, ",", {} });
+
+ joined.insert(joined.end(), raw[i].begin(), raw[i].end());
+ }
+
+ return joined;
+ };
+
+ const auto expanded_varargs = [&]() -> std::vector<PpToken>
+ {
+ std::vector<PpToken> joined;
+
+ for (size_t i = def.params.size(); i < expanded.size(); ++i)
+ {
+ if (i != def.params.size())
+ joined.push_back(PpToken{ PpKind::Punct, ",", {} });
+
+ joined.insert(joined.end(), expanded[i].begin(), expanded[i].end());
+ }
+
+ return joined;
+ };
+
+ const auto& body = def.body;
+
+ for (size_t i = 0; i < body.size(); ++i)
+ {
+ const PpToken& tok = body[i];
+
+ if (tok.text == "#" && def.function_like && i + 1 < body.size() &&
+ body[i + 1].kind == PpKind::Identifier)
+ {
+ if (const int index = param_index(def, body[i + 1].text); index >= 0)
+ {
+ out.push_back(stringize(raw_tokens(index)));
+ ++i;
+ continue;
+ }
+
+ if (def.variadic && body[i + 1].text == "__VA_ARGS__")
+ {
+ out.push_back(stringize(raw_varargs()));
+ ++i;
+ continue;
+ }
+ }
+
+ if (tok.text == "##" && !out.empty() && i + 1 < body.size())
+ {
+ const PpToken& rhs = body[i + 1];
+ std::vector<PpToken> pieces;
+
+ if (const int index = param_index(def, rhs.text); index >= 0)
+ pieces = raw_tokens(index);
+ else if (def.variadic && rhs.text == "__VA_ARGS__")
+ pieces = raw_varargs();
+ else
+ pieces = { rhs };
+
+ if (!pieces.empty())
+ {
+ out.back() = paste_token(out.back().text + pieces.front().text);
+ out.insert(out.end(), pieces.begin() + 1, pieces.end());
+ }
+
+ ++i;
+ continue;
+ }
+
+ const bool paste_operand =
+ (i + 1 < body.size() && body[i + 1].text == "##") ||
+ (i > 0 && body[i - 1].text == "##");
+
+ if (const int index = param_index(def, tok.text); index >= 0)
+ {
+ const std::vector<PpToken> value =
+ paste_operand ? raw_tokens(index) : expanded_tokens(index);
+ out.insert(out.end(), value.begin(), value.end());
+ continue;
+ }
+
+ if (def.variadic && tok.text == "__VA_ARGS__")
+ {
+ const std::vector<PpToken> value =
+ paste_operand ? raw_varargs() : expanded_varargs();
+ out.insert(out.end(), value.begin(), value.end());
+ continue;
+ }
+
+ out.push_back(tok);
+ }
+
+ return out;
+ }
+
+ auto paint(std::vector<PpToken>& tokens, const std::vector<std::string>& carried,
+ const std::string& name) -> void
+ {
+ for (PpToken& token : tokens)
+ {
+ for (const std::string& hidden : carried)
+ {
+ if (!hidden_by(token, hidden))
+ token.hide.push_back(hidden);
+ }
+
+ if (!hidden_by(token, name))
+ token.hide.push_back(name);
+ }
+ }
+
+ auto collect_args(const std::vector<PpToken>& tokens, size_t open, ArgList& args,
+ size_t& end) -> bool
+ {
+ int depth = 0;
+ std::vector<PpToken> current;
+ bool any = false;
+
+ for (size_t i = open; i < tokens.size(); ++i)
+ {
+ const PpToken& tok = tokens[i];
+
+ if (tok.text == "(")
+ {
+ ++depth;
+
+ if (depth == 1)
+ continue;
+ }
+ else if (tok.text == ")")
+ {
+ --depth;
+
+ if (depth == 0)
+ {
+ if (any || !current.empty())
+ args.push_back(std::move(current));
+
+ end = i + 1;
+ return true;
+ }
+ }
+ else if (tok.text == "," && depth == 1)
+ {
+ args.push_back(std::move(current));
+ current.clear();
+ any = true;
+ continue;
+ }
+
+ current.push_back(tok);
+ any = true;
+ }
+
+ return false;
+ }
+
+ auto arity_ok(const MacroDef& def, const ArgList& args) -> bool
+ {
+ const size_t provided =
+ args.size() == 1 && args.front().empty() ? 0 : args.size();
+
+ if (def.variadic)
+ return provided >= def.params.size();
+
+ return provided == def.params.size();
+ }
+
+ auto expand_pass(const MacroTable& table, const std::vector<PpToken>& in,
+ std::vector<PpToken>& out, std::vector<std::string>& expanded) -> bool
+ {
+ bool changed = false;
+ size_t i = 0;
+
+ while (i < in.size())
+ {
+ const PpToken& tok = in[i];
+
+ if (tok.kind != PpKind::Identifier || hidden_by(tok, tok.text))
+ {
+ out.push_back(tok);
+ ++i;
+ continue;
+ }
+
+ const MacroDef* def = table.find(tok.text);
+
+ if (def == nullptr)
+ {
+ out.push_back(tok);
+ ++i;
+ continue;
+ }
+
+ if (!def->function_like)
+ {
+ std::vector<PpToken> repl = substitute(*def, {}, {});
+ paint(repl, tok.hide, def->name);
+ out.insert(out.end(), repl.begin(), repl.end());
+ expanded.push_back(def->name);
+ changed = true;
+ ++i;
+ continue;
+ }
+
+ const size_t paren = i + 1;
+
+ if (paren >= in.size() || in[paren].text != "(")
+ {
+ out.push_back(tok);
+ ++i;
+ continue;
+ }
+
+ ArgList args;
+ size_t end = 0;
+
+ if (!collect_args(in, paren, args, end) || !arity_ok(*def, args))
+ {
+ out.push_back(tok);
+ ++i;
+ continue;
+ }
+
+ ArgList expanded_args;
+ expanded_args.reserve(args.size());
+
+ for (const std::vector<PpToken>& arg : args)
+ expanded_args.push_back(expand_full(table, arg));
+
+ std::vector<PpToken> repl = substitute(*def, args, expanded_args);
+
+ std::vector<std::string> carried = tok.hide;
+ const std::vector<std::string>& close_hide = in[end - 1].hide;
+
+ std::erase_if(carried, [&](const std::string& name)
+ {
+ return std::ranges::find(close_hide, name) == close_hide.end();
+ });
+
+ paint(repl, carried, def->name);
+ out.insert(out.end(), repl.begin(), repl.end());
+ expanded.push_back(def->name);
+ changed = true;
+ i = end;
+ }
+
+ return changed && out.size() < MAX_TOKENS;
+ }
+
+ auto expand_full(const MacroTable& table, std::vector<PpToken> tokens)
+ -> std::vector<PpToken>
+ {
+ for (int level = 0; level < 64; ++level)
+ {
+ std::vector<PpToken> next;
+ std::vector<std::string> expanded;
+
+ if (!expand_pass(table, tokens, next, expanded) || expanded.empty())
+ break;
+
+ tokens = std::move(next);
+
+ if (tokens.size() >= MAX_TOKENS)
+ break;
+ }
+
+ return tokens;
+ }
+ }
+
+ auto MacroTable::clear() -> void
+ {
+ m_macros.clear();
+ ++m_revision;
+ }
+
+ auto MacroTable::find(std::string_view name) const -> const MacroDef*
+ {
+ const auto it = m_macros.find(std::string(name));
+ return it == m_macros.end() ? nullptr : &it->second;
+ }
+
+ auto MacroTable::build(const std::vector<std::string>& lines,
+ const std::filesystem::path& base_dir) -> void
+ {
+ m_macros.clear();
+
+ std::vector<std::filesystem::path> seen;
+ scan_lines(lines, base_dir, 0, seen);
+
+ ++m_revision;
+ }
+
+ namespace
+ {
+ auto splice_lines(const std::vector<std::string>& lines) -> std::vector<std::string>
+ {
+ std::vector<std::string> out;
+ std::string pending;
+ bool continuing = false;
+
+ for (const std::string& line : lines)
+ {
+ std::string text = line;
+
+ const bool cont = !text.empty() && text.back() == '\\';
+
+ if (cont)
+ text.pop_back();
+
+ if (continuing)
+ pending += text;
+ else
+ pending = text;
+
+ if (cont)
+ {
+ continuing = true;
+ continue;
+ }
+
+ out.push_back(std::move(pending));
+ pending.clear();
+ continuing = false;
+ }
+
+ if (continuing)
+ out.push_back(std::move(pending));
+
+ return out;
+ }
+
+ auto directive_of(const std::string& line, std::string& rest) -> std::string
+ {
+ size_t at = 0;
+
+ while (at < line.size() && std::isspace(static_cast<unsigned char>(line[at])) != 0)
+ ++at;
+
+ if (at >= line.size() || line[at] != '#')
+ return {};
+
+ ++at;
+
+ while (at < line.size() && std::isspace(static_cast<unsigned char>(line[at])) != 0)
+ ++at;
+
+ const size_t start = at;
+
+ while (at < line.size() && is_ident(line[at]))
+ ++at;
+
+ std::string word = line.substr(start, at - start);
+ rest = line.substr(at);
+ return word;
+ }
+
+ auto quote_include(const std::string& rest) -> std::string
+ {
+ const size_t open = rest.find('"');
+
+ if (open == std::string::npos)
+ return {};
+
+ const size_t close = rest.find('"', open + 1);
+
+ if (close == std::string::npos)
+ return {};
+
+ return rest.substr(open + 1, close - open - 1);
+ }
+
+ auto parse_define(const std::string& rest, MacroDef& def) -> bool
+ {
+ size_t at = 0;
+
+ while (at < rest.size() && std::isspace(static_cast<unsigned char>(rest[at])) != 0)
+ ++at;
+
+ if (at >= rest.size() || !is_ident_start(rest[at]))
+ return false;
+
+ const size_t name_start = at;
+
+ while (at < rest.size() && is_ident(rest[at]))
+ ++at;
+
+ def.name = rest.substr(name_start, at - name_start);
+
+ if (at < rest.size() && rest[at] == '(')
+ {
+ def.function_like = true;
+ ++at;
+
+ while (true)
+ {
+ while (at < rest.size() &&
+ std::isspace(static_cast<unsigned char>(rest[at])) != 0)
+ ++at;
+
+ if (at < rest.size() && rest[at] == ')')
+ {
+ ++at;
+ break;
+ }
+
+ if (rest.compare(at, 3, "...") == 0)
+ {
+ def.variadic = true;
+ at += 3;
+ continue;
+ }
+
+ if (at < rest.size() && is_ident_start(rest[at]))
+ {
+ const size_t p = at;
+
+ while (at < rest.size() && is_ident(rest[at]))
+ ++at;
+
+ std::string param = rest.substr(p, at - p);
+
+ if (rest.compare(at, 3, "...") == 0)
+ {
+ def.variadic = true;
+ at += 3;
+ }
+ else
+ {
+ def.params.push_back(std::move(param));
+ }
+
+ continue;
+ }
+
+ if (at < rest.size() && rest[at] == ',')
+ {
+ ++at;
+ continue;
+ }
+
+ return false;
+ }
+ }
+
+ def.body = tokenize(std::string_view(rest).substr(at));
+ return true;
+ }
+ }
+
+ auto MacroTable::scan_lines(const std::vector<std::string>& lines,
+ const std::filesystem::path& base_dir, int depth,
+ std::vector<std::filesystem::path>& seen) -> void
+ {
+ const std::vector<std::string> logical = splice_lines(lines);
+
+ for (const std::string& line : logical)
+ {
+ std::string rest;
+ const std::string directive = directive_of(line, rest);
+
+ if (directive == "define")
+ {
+ MacroDef def;
+
+ if (parse_define(rest, def))
+ m_macros[def.name] = std::move(def);
+ }
+ else if (directive == "undef")
+ {
+ MacroDef tmp;
+
+ if (parse_define(rest, tmp))
+ m_macros.erase(tmp.name);
+ }
+ else if (directive == "include" && depth < MAX_INCLUDE_DEPTH && !base_dir.empty())
+ {
+ const std::string name = quote_include(rest);
+
+ if (!name.empty())
+ scan_path(base_dir / name, depth + 1, seen);
+ }
+ }
+ }
+
+ auto MacroTable::scan_path(const std::filesystem::path& path, int depth,
+ std::vector<std::filesystem::path>& seen) -> void
+ {
+ std::error_code error;
+ const std::filesystem::path canonical = std::filesystem::weakly_canonical(path, error);
+ const std::filesystem::path key = error ? path : canonical;
+
+ if (std::ranges::find(seen, key) != seen.end())
+ return;
+
+ seen.push_back(key);
+
+ std::ifstream file(path);
+
+ if (!file.is_open())
+ return;
+
+ std::vector<std::string> lines;
+ std::string line;
+
+ while (std::getline(file, line))
+ {
+ if (!line.empty() && line.back() == '\r')
+ line.pop_back();
+
+ lines.push_back(std::move(line));
+ }
+
+ scan_lines(lines, path.parent_path(), depth, seen);
+ }
+
+ auto expand_stages(const MacroTable& table, std::string_view input, int max_levels)
+ -> MacroExpansion
+ {
+ MacroExpansion result;
+
+ std::vector<PpToken> tokens = tokenize(input);
+
+ result.levels.push_back(render(tokens));
+ result.expanded.emplace_back();
+
+ for (int level = 0; level < max_levels; ++level)
+ {
+ std::vector<PpToken> next;
+ std::vector<std::string> expanded;
+
+ if (!expand_pass(table, tokens, next, expanded) || expanded.empty())
+ {
+ result.m_reached_fixpoint = true;
+ return result;
+ }
+
+ tokens = std::move(next);
+ result.levels.push_back(render(tokens));
+ result.expanded.push_back(std::move(expanded));
+
+ if (tokens.size() >= MAX_TOKENS)
+ {
+ result.m_reached_fixpoint = false;
+ return result;
+ }
+ }
+
+ result.m_reached_fixpoint = false;
+ return result;
+ }
+}
diff --git a/src/ui/macro_expander.h b/src/ui/macro_expander.h
new file mode 100644
index 0000000..3352a53
--- /dev/null
+++ b/src/ui/macro_expander.h
@@ -0,0 +1,76 @@
+#pragma once
+
+#include <cstdint>
+#include <filesystem>
+#include <string>
+#include <string_view>
+#include <unordered_map>
+#include <vector>
+
+namespace Hsdbg
+{
+ enum class PpKind : uint8_t
+ {
+ Identifier,
+ Number,
+ String,
+ Char,
+ Punct,
+ };
+
+ struct PpToken
+ {
+ PpKind kind = PpKind::Identifier;
+ std::string text;
+ std::vector<std::string> hide;
+ };
+
+ struct MacroDef
+ {
+ std::string name;
+ bool function_like = false;
+ bool variadic = false;
+ std::vector<std::string> params;
+ std::vector<PpToken> body;
+ };
+
+ class MacroTable
+ {
+ public:
+ auto clear() -> void;
+
+ auto build(const std::vector<std::string>& lines,
+ const std::filesystem::path& base_dir) -> void;
+
+ auto find(std::string_view name) const -> const MacroDef*;
+
+ auto empty() const -> bool { return m_macros.empty(); }
+ auto size() const -> size_t { return m_macros.size(); }
+
+ auto revision() const -> uint64_t { return m_revision; }
+
+ private:
+ auto scan_lines(const std::vector<std::string>& lines,
+ const std::filesystem::path& base_dir, int depth,
+ std::vector<std::filesystem::path>& seen) -> void;
+ auto scan_path(const std::filesystem::path& path, int depth,
+ std::vector<std::filesystem::path>& seen) -> void;
+
+ std::unordered_map<std::string, MacroDef> m_macros;
+ uint64_t m_revision = 0;
+ };
+
+ struct MacroExpansion
+ {
+ std::vector<std::string> levels;
+
+ std::vector<std::vector<std::string>> expanded;
+
+ auto fully_expanded() const -> bool { return m_reached_fixpoint; }
+
+ bool m_reached_fixpoint = true;
+ };
+
+ auto expand_stages(const MacroTable& table, std::string_view input,
+ int max_levels = 64) -> MacroExpansion;
+}
diff --git a/src/ui/source_view.cpp b/src/ui/source_view.cpp
index e0c26b4..5ced658 100644
--- a/src/ui/source_view.cpp
+++ b/src/ui/source_view.cpp
@@ -100,7 +100,8 @@ namespace Hsdbg
return true;
}
- auto highlight_lines(const std::vector<std::string>& lines, Language language)
+ auto highlight_lines(const std::vector<std::string>& lines, Language language,
+ const MacroTable& macros)
-> std::vector<std::vector<SourceSpan>>
{
std::vector<std::vector<SourceSpan>> out(lines.size());
@@ -260,9 +261,15 @@ namespace Hsdbg
while (at < size && is_word(line[at]))
++at;
- if (const SyntaxKind kind = classify_word(
- std::string_view(line).substr(start, at - start), language);
- kind != SyntaxKind::Default)
+ const std::string_view word =
+ std::string_view(line).substr(start, at - start);
+ SyntaxKind kind = classify_word(word, language);
+
+ if (kind == SyntaxKind::Default && language == Language::Cpp &&
+ macros.find(word) != nullptr)
+ kind = SyntaxKind::Macro;
+
+ if (kind != SyntaxKind::Default)
{
flush_default(start);
emit(start, at - start, kind);
@@ -314,6 +321,42 @@ namespace Hsdbg
{
return ImGui::CalcTextSize(std::to_string(line_count).c_str()).x;
}
+
+ auto capture_invocation(const std::string& line, uint32_t start) -> std::string
+ {
+ const size_t size = line.size();
+ size_t at = start;
+
+ while (at < size && is_word(line[at]))
+ ++at;
+
+ size_t paren = at;
+
+ while (paren < size && std::isspace(static_cast<unsigned char>(line[paren])) != 0)
+ ++paren;
+
+ if (paren < size && line[paren] == '(')
+ {
+ int depth = 0;
+
+ for (size_t i = paren; i < size; ++i)
+ {
+ if (line[i] == '(')
+ {
+ ++depth;
+ }
+ else if (line[i] == ')')
+ {
+ --depth;
+
+ if (depth == 0)
+ return line.substr(start, i + 1 - start);
+ }
+ }
+ }
+
+ return line.substr(start, at - start);
+ }
}
auto SourceView::open(const std::filesystem::path& path) -> Result<void>
@@ -338,7 +381,13 @@ namespace Hsdbg
m_path = path;
m_lines = std::move(lines);
m_highlight = language.has_value();
- m_spans = language ? highlight_lines(m_lines, *language)
+
+ m_macros.clear();
+
+ if (language == Language::Cpp)
+ m_macros.build(m_lines, path.parent_path());
+
+ m_spans = language ? highlight_lines(m_lines, *language, m_macros)
: std::vector<std::vector<SourceSpan>>{};
m_path_input = path.string();
m_error.clear();
@@ -354,11 +403,20 @@ namespace Hsdbg
m_path.clear();
m_lines.clear();
m_spans.clear();
+ m_macros.clear();
+ m_macro_request.reset();
m_highlight = false;
m_error.clear();
m_highlighted_line = 0;
}
+ auto SourceView::take_macro_request() -> std::optional<std::string>
+ {
+ std::optional<std::string> request = std::move(m_macro_request);
+ m_macro_request.reset();
+ return request;
+ }
+
auto SourceView::set_highlighted_line(uint32_t line) -> void
{
m_highlighted_line = line;
@@ -533,6 +591,10 @@ namespace Hsdbg
{
ImGui::TextUnformatted(begin, end);
}
+ else if (span.kind == SyntaxKind::Macro)
+ {
+ draw_macro_span(draw_list, text, span);
+ }
else
{
ImGui::PushStyleColor(ImGuiCol_Text,
@@ -549,4 +611,64 @@ namespace Hsdbg
ImGui::EndChild();
}
+
+ auto SourceView::draw_macro_span(ImDrawList* draw_list, const std::string& line,
+ const SourceSpan& span) -> void
+ {
+ const char* const begin = line.c_str() + span.start;
+ const char* const end = begin + span.length;
+ const ImU32 color = m_syntax_colors[static_cast<size_t>(SyntaxKind::Macro)];
+
+ ImGui::PushStyleColor(ImGuiCol_Text, color);
+ ImGui::TextUnformatted(begin, end);
+ ImGui::PopStyleColor();
+
+ if (!ImGui::IsItemHovered())
+ return;
+
+ const ImVec2 rect_min = ImGui::GetItemRectMin();
+ const ImVec2 rect_max = ImGui::GetItemRectMax();
+ draw_list->AddLine(ImVec2(rect_min.x, rect_max.y - 1.0f),
+ ImVec2(rect_max.x, rect_max.y - 1.0f), color);
+
+ const std::string invocation = capture_invocation(line, span.start);
+ const MacroExpansion expansion = expand_stages(m_macros, invocation);
+
+ ImGui::BeginTooltip();
+
+ ImGui::PushStyleColor(ImGuiCol_Text, color);
+ ImGui::TextUnformatted(invocation.c_str());
+ ImGui::PopStyleColor();
+
+ ImGui::Separator();
+
+ std::string preview = expansion.levels.back();
+
+ if (constexpr size_t limit = 240; preview.size() > limit)
+ {
+ preview.resize(limit);
+ preview += " ...";
+ }
+
+ ImGui::PushTextWrapPos(ImGui::GetFontSize() * 30.0f);
+ ImGui::TextUnformatted(preview.c_str());
+ ImGui::PopTextWrapPos();
+
+ ImGui::Spacing();
+
+ const size_t steps = expansion.levels.size() - 1;
+
+ if (steps == 0)
+ ImGui::TextDisabled("no expansion");
+ else if (!expansion.fully_expanded())
+ ImGui::TextDisabled("%zu+ levels — click to step through", steps);
+ else
+ ImGui::TextDisabled("%zu level%s — click to step through", steps,
+ steps == 1 ? "" : "s");
+
+ ImGui::EndTooltip();
+
+ if (ImGui::IsMouseClicked(ImGuiMouseButton_Left))
+ m_macro_request = invocation;
+ }
}
diff --git a/src/ui/source_view.h b/src/ui/source_view.h
index dd3ed27..6750d17 100644
--- a/src/ui/source_view.h
+++ b/src/ui/source_view.h
@@ -1,12 +1,16 @@
#pragma once
#include "core/result.h"
+#include "ui/macro_expander.h"
#include <cstdint>
#include <filesystem>
+#include <optional>
#include <string>
#include <vector>
+struct ImDrawList;
+
namespace Hsdbg
{
class Debugger;
@@ -20,6 +24,7 @@ namespace Hsdbg
Number,
Comment,
Preprocessor,
+ Macro,
};
struct SourceSpan
@@ -49,20 +54,30 @@ namespace Hsdbg
auto line_count() const -> size_t { return m_lines.size(); }
auto is_open() const -> bool { return !m_lines.empty(); }
+ auto macros() const -> const MacroTable& { return m_macros; }
+
+ auto take_macro_request() -> std::optional<std::string>;
+
private:
auto draw_open_bar() -> void;
auto draw_lines(Debugger& debugger) -> void;
+ auto draw_macro_span(ImDrawList* draw_list, const std::string& line,
+ const SourceSpan& span) -> void;
+
std::filesystem::path m_path;
std::vector<std::string> m_lines;
std::vector<std::vector<SourceSpan>> m_spans;
+ MacroTable m_macros;
+ std::optional<std::string> m_macro_request;
bool m_highlight = false;
bool m_highlighting_enabled = true;
bool m_line_numbers = true;
bool m_highlight_current_line = true;
- unsigned int m_syntax_colors[7] = {
+ unsigned int m_syntax_colors[8] = {
0, 0xFFD69C56, 0xFFB0C94E, 0xFF7891CE, 0xFFA8CEB5, 0xFF55996A, 0xFFC086C5,
+ 0xFF4FA3E0,
};
unsigned int m_current_line_color = 0xFF2E483A;
std::string m_path_input;