aboutsummaryrefslogtreecommitdiff
path: root/src/ui
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui')
-rw-r--r--src/ui/macro_expander.cpp847
-rw-r--r--src/ui/macro_expander.h100
-rw-r--r--src/ui/source_view.cpp141
-rw-r--r--src/ui/source_view.h28
-rw-r--r--src/ui/ui.cpp286
-rw-r--r--src/ui/ui.h11
6 files changed, 1404 insertions, 9 deletions
diff --git a/src/ui/macro_expander.cpp b/src/ui/macro_expander.cpp
new file mode 100644
index 0000000..f4588c3
--- /dev/null
+++ b/src/ui/macro_expander.cpp
@@ -0,0 +1,847 @@
+#include "ui/macro_expander.h"
+
+#include <algorithm>
+#include <cctype>
+#include <fstream>
+
+namespace Hsdbg
+{
+ namespace
+ {
+ // hard ceilings so a runaway macro can never take the whole ui with it
+ 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 == '_';
+ }
+
+ // pull one preprocessing token out of text starting at index. strings and
+ // character literals are kept whole, and multi-character punctuators the
+ // expander actually reasons about (##, ::, ->) are recognised so they do
+ // not split. everything else falls back to a single-character punctuator
+ 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++;
+
+ // a pp-number swallows digits, letters, dots and the exponent
+ // signs, which is loose but matches how the standard lexes them
+ 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();
+ }
+
+ // whether a space belongs between two adjacent tokens when rendering back
+ // to text; purely cosmetic, aimed at readable c-ish output
+ 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;
+ }
+
+ // build a string literal token out of an argument's spelling, escaping the
+ // way # is required to: backslashes and quotes inside the text survive
+ 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();
+
+ // the paste did not form a single clean token; keep the joined
+ // spelling so the user still sees what ## produced
+ return PpToken{ PpKind::Identifier, text, {} };
+ }
+
+ // the arguments handed to a function-like macro. an object-like macro just
+ // passes an empty list through
+ using ArgList = std::vector<std::vector<PpToken>>;
+
+ // run every expansion layer on tokens until nothing changes, used to
+ // fully expand an argument before it is pasted into a macro body
+ auto expand_full(const MacroTable& table, std::vector<PpToken> tokens)
+ -> std::vector<PpToken>;
+
+ // raw holds each argument exactly as written (what # and ## must see);
+ // expanded holds the same arguments after full macro expansion (what a
+ // plain parameter reference is replaced with, per the standard)
+ 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 {};
+ };
+
+ // __VA_ARGS__ from the raw arguments, for # and ##
+ 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;
+ }
+
+ // a parameter next to ## on either side keeps its raw argument;
+ // everywhere else it takes the fully expanded one
+ 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;
+ }
+
+ // add the just-expanded macro's name to the hide set of every produced
+ // token, folding in the hide set the invocation itself carried
+ 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);
+ }
+ }
+
+ // gather the arguments of a function-like call. open is the index of '(';
+ // on success end is set past the matching ')'. returns false if the parens
+ // never balance, in which case the name is left as a plain identifier
+ 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;
+ }
+
+ // whether a call supplies enough arguments for a definition. a zero-arg
+ // macro invoked as NAME() lexes as a single empty argument, so treat that
+ // as a match
+ 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();
+ }
+
+ // one expansion layer: every eligible macro name in the stream is replaced
+ // once, and the tokens it produced are left for the next layer to rescan.
+ // that "one layer per pass" rule is what the level slider steps through
+ 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;
+
+ // function-like painting keeps only the names hidden at both the
+ // name and the closing paren, then adds this macro
+ 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
+ {
+ // stitch backslash-newline continuations back into single logical lines so
+ // a multi-line #define parses as one
+ 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);
+
+ // a '(' touching the name with no space makes it function-like; a
+ // space means the paren is part of the body
+ 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);
+
+ // a named variadic (GNU args...) collapses onto the
+ // standard __VA_ARGS__ handling
+ 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;
+ }
+
+ // something unexpected in the parameter list; give up on this
+ // definition rather than guess
+ 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;
+ }
+ }
+
+ // ran out of levels before settling; the last one may still expand further
+ 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..cf87d86
--- /dev/null
+++ b/src/ui/macro_expander.h
@@ -0,0 +1,100 @@
+#pragma once
+
+#include <cstdint>
+#include <filesystem>
+#include <string>
+#include <string_view>
+#include <unordered_map>
+#include <vector>
+
+namespace Hsdbg
+{
+ // one preprocessing token. whitespace is dropped during tokenising, so a run
+ // of tokens is rendered back to text with a small spacing heuristic. the hide
+ // set is the classic preprocessor "blue paint": the names of macros that must
+ // not expand this token again, which is what keeps a self-referential macro
+ // from unrolling forever
+ enum class PpKind : uint8_t
+ {
+ Identifier,
+ Number,
+ String,
+ Char,
+ Punct,
+ };
+
+ struct PpToken
+ {
+ PpKind kind = PpKind::Identifier;
+ std::string text;
+ std::vector<std::string> hide;
+ };
+
+ // a single #define, object-like (BUFFER) or function-like (MAX(a, b)). the
+ // body is the replacement list as significant tokens, with #, ## and the
+ // parameter names left in place for substitution time
+ struct MacroDef
+ {
+ std::string name;
+ bool function_like = false;
+ bool variadic = false;
+ std::vector<std::string> params;
+ std::vector<PpToken> body;
+ };
+
+ // the set of #defines visible in a source file. built by scanning the file
+ // for #define / #undef directives in order, optionally following local
+ // "quote" includes one project deep. system <...> includes are ignored on
+ // purpose: this is meant for a project's own macros, not libc's
+ class MacroTable
+ {
+ public:
+ auto clear() -> void;
+
+ // scan already-loaded lines as the primary file, then chase local quote
+ // includes relative to base_dir off disk. either argument may be empty
+ 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(); }
+
+ // bumped every rebuild, so a consumer can tell its cached expansion is
+ // stale without comparing the whole table
+ 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;
+ };
+
+ // the result of unrolling an expression one macro layer at a time. levels[0]
+ // is the input as written, levels[i] is the text after i rounds of expansion,
+ // and levels.back() is the fixpoint where nothing expands any further
+ struct MacroExpansion
+ {
+ std::vector<std::string> levels;
+
+ // names of macros that were actually expanded going into each level, so
+ // levels_expanded[i] describes what turned levels[i-1] into levels[i].
+ // index 0 is always empty
+ std::vector<std::vector<std::string>> expanded;
+
+ auto fully_expanded() const -> bool { return m_reached_fixpoint; }
+
+ bool m_reached_fixpoint = true;
+ };
+
+ // unroll input against the table, capping the number of layers so a
+ // pathological macro cannot hang the ui
+ 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 b998e3f..7638f17 100644
--- a/src/ui/source_view.cpp
+++ b/src/ui/source_view.cpp
@@ -103,7 +103,8 @@ namespace Hsdbg
// splits every line into contiguous coloured spans. the whole file is
// walked in order so a block comment opened on one line stays open on the
// next, which a per-line pass could not know
- 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());
@@ -265,9 +266,17 @@ 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);
+
+ // a plain identifier that names a #define is coloured as a
+ // macro so it reads as expandable in the source view
+ 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);
@@ -321,6 +330,46 @@ namespace Hsdbg
{
return ImGui::CalcTextSize(std::to_string(line_count).c_str()).x;
}
+
+ // grab a macro invocation out of a line starting at the name: the name
+ // alone for an object-like use, or the name plus a balanced argument list
+ // for a function-like one. a call that runs off the end of the line falls
+ // back to the bare name rather than guessing where it closes
+ 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>
@@ -345,7 +394,15 @@ namespace Hsdbg
m_path = path;
m_lines = std::move(lines);
m_highlight = language.has_value();
- m_spans = language ? highlight_lines(m_lines, *language)
+
+ // gather the file's #defines (chasing local quote includes) so macro
+ // names highlight and the macros panel has something to expand
+ 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();
@@ -361,11 +418,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;
@@ -541,6 +607,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,
@@ -557,4 +627,65 @@ 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;
+
+ // underline on hover so the name reads as a link
+ 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 2e1a20f..c8a9a21 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;
@@ -22,6 +26,7 @@ namespace Hsdbg
Number,
Comment,
Preprocessor,
+ Macro,
};
struct SourceSpan
@@ -52,21 +57,40 @@ namespace Hsdbg
auto line_count() const -> size_t { return m_lines.size(); }
auto is_open() const -> bool { return !m_lines.empty(); }
+ // the #defines visible in the open file, so a panel can expand the same
+ // macros the source view is highlighting
+ auto macros() const -> const MacroTable& { return m_macros; }
+
+ // when the user clicks a highlighted macro, the invocation as written
+ // (name plus any argument list) is stashed here for the ui to pick up and
+ // hand to the macros panel; cleared once taken
+ auto take_macro_request() -> std::optional<std::string>;
+
private:
auto draw_open_bar() -> void;
auto draw_lines(Debugger& debugger) -> void;
+ // renders a highlighted macro name: hovering previews its expansion,
+ // clicking records the invocation for the macros panel to open
+ 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;
- // indexed by SyntaxKind; the defaults match the built-in dark palette
- unsigned int m_syntax_colors[7] = {
+ // indexed by SyntaxKind; the defaults match the built-in dark palette.
+ // the macro colour (last) is warm on purpose so expandable names read as
+ // clickable against the rest of the syntax
+ 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;
diff --git a/src/ui/ui.cpp b/src/ui/ui.cpp
index e1a2f9d..f39b95f 100644
--- a/src/ui/ui.cpp
+++ b/src/ui/ui.cpp
@@ -16,6 +16,7 @@
#include <cfloat>
#include <filesystem>
#include <format>
+#include <optional>
#include <span>
#include <vector>
@@ -38,6 +39,8 @@ namespace Hsdbg
constexpr const char* PANEL_DISASSEMBLY = "disassembly";
constexpr const char* PANEL_CONSOLE = "console";
constexpr const char* PANEL_PROFILER = "profiler";
+ constexpr const char* PANEL_TIMELINE = "timeline";
+ constexpr const char* PANEL_MACROS = "macros";
constexpr const char* LOAD_TARGET_POPUP = "load target";
@@ -614,6 +617,17 @@ namespace Hsdbg
ImGui::End();
draw_source_panel(debugger);
+
+ // a click on a highlighted macro in the source view loads it into the
+ // macros panel, brings the panel up and jumps its focus there
+ if (std::optional<std::string> request = m_source_view.take_macro_request())
+ {
+ m_macro_input = std::move(*request);
+ m_macro_level = 0;
+ m_visible.macros = true;
+ m_focus_macros = true;
+ }
+
draw_breakpoints_panel(debugger);
draw_call_stack_panel(debugger);
draw_source_tree_panel(debugger);
@@ -624,6 +638,8 @@ namespace Hsdbg
draw_disassembly_panel(debugger);
draw_console_panel(debugger);
draw_profiler_panel(debugger);
+ draw_timeline_panel(debugger);
+ draw_macros_panel(debugger);
if (m_visible.demo)
ImGui::ShowDemoWindow(&m_visible.demo);
@@ -657,6 +673,12 @@ namespace Hsdbg
ImGui::SetWindowFocus(PANEL_BREAKPOINTS);
m_focus_breakpoints = false;
}
+
+ if (m_focus_macros)
+ {
+ ImGui::SetWindowFocus(PANEL_MACROS);
+ m_focus_macros = false;
+ }
}
auto Ui::build_default_layout(uint32_t dockspace_id) -> void
@@ -678,6 +700,7 @@ namespace Hsdbg
ImGui::DockBuilderDockWindow(PANEL_SOURCE, center_id);
ImGui::DockBuilderDockWindow(PANEL_DISASSEMBLY, center_id);
+ ImGui::DockBuilderDockWindow(PANEL_TIMELINE, center_id);
ImGui::DockBuilderDockWindow(PANEL_SOURCE_TREE, left_top_id);
ImGui::DockBuilderDockWindow(PANEL_THREADS, left_top_id);
ImGui::DockBuilderDockWindow(PANEL_SYMBOLS, left_bottom_id);
@@ -687,6 +710,7 @@ namespace Hsdbg
ImGui::DockBuilderDockWindow(PANEL_BREAKPOINTS, bottom_id);
ImGui::DockBuilderDockWindow(PANEL_CONSOLE, bottom_id);
ImGui::DockBuilderDockWindow(PANEL_PROFILER, bottom_id);
+ ImGui::DockBuilderDockWindow(PANEL_MACROS, bottom_id);
ImGui::DockBuilderFinish(dockspace_id);
}
@@ -769,6 +793,8 @@ namespace Hsdbg
ImGui::MenuItem(PANEL_DISASSEMBLY, nullptr, &m_visible.disassembly);
ImGui::MenuItem(PANEL_CONSOLE, nullptr, &m_visible.console);
ImGui::MenuItem(PANEL_PROFILER, nullptr, &m_visible.profiler);
+ ImGui::MenuItem(PANEL_TIMELINE, nullptr, &m_visible.timeline);
+ ImGui::MenuItem(PANEL_MACROS, nullptr, &m_visible.macros);
ImGui::Separator();
@@ -1770,10 +1796,13 @@ namespace Hsdbg
constexpr ImGuiTableFlags flags = ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV |
ImGuiTableFlags_SizingStretchProp;
- if (ImGui::BeginTable("##traces", 3, flags))
+ if (ImGui::BeginTable("##traces", 6, flags))
{
ImGui::TableSetupColumn("function");
- ImGui::TableSetupColumn("calls", ImGuiTableColumnFlags_WidthFixed, 70.0f);
+ ImGui::TableSetupColumn("calls", ImGuiTableColumnFlags_WidthFixed, 52.0f);
+ ImGui::TableSetupColumn("avg", ImGuiTableColumnFlags_WidthFixed, 72.0f);
+ ImGui::TableSetupColumn("min", ImGuiTableColumnFlags_WidthFixed, 72.0f);
+ ImGui::TableSetupColumn("max", ImGuiTableColumnFlags_WidthFixed, 72.0f);
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 24.0f);
ImGui::TableHeadersRow();
@@ -1781,6 +1810,11 @@ namespace Hsdbg
for (const FunctionTrace& trace : traces)
{
+ // milliseconds read easier than the seconds we store
+ const double avg_ms = trace.completed_count > 0
+ ? trace.total_time / static_cast<double>(trace.completed_count) * 1000.0
+ : 0.0;
+
ImGui::TableNextRow();
ImGui::TableNextColumn();
@@ -1790,6 +1824,24 @@ namespace Hsdbg
ImGui::Text("%llu", static_cast<unsigned long long>(trace.call_count));
ImGui::TableNextColumn();
+ if (trace.completed_count > 0)
+ ImGui::Text("%.3f ms", avg_ms);
+ else
+ ImGui::TextDisabled("-");
+
+ ImGui::TableNextColumn();
+ if (trace.completed_count > 0)
+ ImGui::Text("%.3f ms", trace.min_time * 1000.0);
+ else
+ ImGui::TextDisabled("-");
+
+ ImGui::TableNextColumn();
+ if (trace.completed_count > 0)
+ ImGui::Text("%.3f ms", trace.max_time * 1000.0);
+ else
+ ImGui::TextDisabled("-");
+
+ ImGui::TableNextColumn();
ImGui::PushID(static_cast<int>(trace.id));
if (ImGui::SmallButton("x"))
remove_id = trace.id;
@@ -1828,6 +1880,236 @@ namespace Hsdbg
ImGui::End();
}
+ auto Ui::draw_timeline_panel(Debugger& debugger) -> void
+ {
+ if (!m_visible.timeline)
+ return;
+
+ if (ImGui::Begin(PANEL_TIMELINE, &m_visible.timeline))
+ {
+ const std::span<const TimelineSpan> spans = debugger.timeline();
+
+ if (debugger.instrumentation_active())
+ {
+ ImGui::TextDisabled("instrumented: every function traced automatically");
+ }
+ else
+ {
+ bool sampling = debugger.sampling_enabled();
+ if (ImGui::Checkbox("sample while running", &sampling))
+ debugger.set_sampling_enabled(sampling);
+
+ ImGui::SameLine();
+ ImGui::TextDisabled("(any binary, approximate)");
+ }
+
+ if (spans.empty())
+ {
+ ImGui::TextDisabled("run an instrumented target, or trace functions, then stop to see calls");
+ }
+ else
+ {
+ // resolve a trace id to its function name for labels and tooltips
+ const auto name_of = [&](uint32_t trace_id) -> const char*
+ {
+ return debugger.span_label(trace_id);
+ };
+
+ // the time range to fit and how tall the call stack gets
+ double t_min = spans.front().start;
+ double t_max = t_min;
+ uint32_t max_depth = 0;
+
+ for (const TimelineSpan& span : spans)
+ {
+ t_min = std::min(t_min, span.start);
+ t_max = std::max(t_max, span.start + span.duration);
+ max_depth = std::max(max_depth, span.depth);
+ }
+
+ const double range = std::max(t_max - t_min, 1.0e-6);
+
+ ImGui::Text("%.3f ms total %zu calls", range * 1000.0, spans.size());
+
+ constexpr float row_height = 20.0f;
+ const float rows = static_cast<float>(max_depth + 1);
+
+ const ImVec2 origin = ImGui::GetCursorScreenPos();
+ const ImVec2 avail = ImGui::GetContentRegionAvail();
+
+ const float canvas_w = std::max(avail.x, 1.0f);
+ const float canvas_h = std::max(avail.y, rows * row_height + 4.0f);
+
+ // claim the region so hovering resolves against it
+ ImGui::InvisibleButton("##timeline_canvas", ImVec2(canvas_w, canvas_h));
+ const bool canvas_hovered = ImGui::IsItemHovered();
+
+ ImDrawList* draw = ImGui::GetWindowDrawList();
+ draw->PushClipRect(origin, ImVec2(origin.x + canvas_w, origin.y + canvas_h), true);
+
+ const float scale = canvas_w / static_cast<float>(range); // pixels per second
+ const float baseline = origin.y + canvas_h; // row 0 rests on the bottom
+
+ const ImVec2 mouse = ImGui::GetMousePos();
+
+ for (const TimelineSpan& span : spans)
+ {
+ const float x0 = origin.x + static_cast<float>((span.start - t_min) * scale);
+ const float width = std::max(1.0f, static_cast<float>(span.duration * scale));
+ const float y1 = baseline - static_cast<float>(span.depth) * row_height;
+ const float y0 = y1 - (row_height - 2.0f);
+
+ const ImU32 fill = ImColor::HSV(span.trace_id * 0.13f, 0.55f, 0.78f);
+
+ draw->AddRectFilled(ImVec2(x0, y0), ImVec2(x0 + width, y1), fill, 2.0f);
+ draw->AddRect(ImVec2(x0, y0), ImVec2(x0 + width, y1), IM_COL32(0, 0, 0, 90), 2.0f);
+
+ if (width > 24.0f)
+ {
+ draw->PushClipRect(ImVec2(x0 + 2.0f, y0), ImVec2(x0 + width - 2.0f, y1), true);
+ draw->AddText(ImVec2(x0 + 4.0f, y0 + 2.0f), IM_COL32(20, 20, 20, 255),
+ name_of(span.trace_id));
+ draw->PopClipRect();
+ }
+
+ const bool over = canvas_hovered && mouse.x >= x0 && mouse.x <= x0 + width &&
+ mouse.y >= y0 && mouse.y <= y1;
+
+ if (over)
+ {
+ ImGui::BeginTooltip();
+ ImGui::TextUnformatted(name_of(span.trace_id));
+ ImGui::Text("start %.3f ms", (span.start - t_min) * 1000.0);
+ if (span.duration > 0.0)
+ ImGui::Text("duration %.3f ms", span.duration * 1000.0);
+ else
+ ImGui::TextDisabled("running...");
+ ImGui::EndTooltip();
+ }
+ }
+
+ draw->PopClipRect();
+ }
+ }
+
+ ImGui::End();
+ }
+
+ auto Ui::draw_macros_panel(Debugger& /*debugger*/) -> void
+ {
+ if (!m_visible.macros)
+ return;
+
+ if (ImGui::Begin(PANEL_MACROS, &m_visible.macros))
+ {
+ const MacroTable& table = m_source_view.macros();
+
+ ImGui::TextDisabled("unroll a #define one layer per level");
+
+ ImGui::SetNextItemWidth(-FLT_MIN);
+
+ if (ImGui::InputTextWithHint("##macro_input",
+ "click a macro in source, or type one like MAX(a, b)",
+ &m_macro_input))
+ m_macro_level = 0;
+
+ if (table.empty())
+ ImGui::TextDisabled("no #define macros found in the open source file");
+
+ if (m_macro_input.empty())
+ {
+ ImGui::End();
+ return;
+ }
+
+ const MacroExpansion expansion = expand_stages(table, m_macro_input);
+ const int max_level = static_cast<int>(expansion.levels.size()) - 1;
+
+ m_macro_level = std::clamp(m_macro_level, 0, max_level);
+
+ ImGui::BeginDisabled(m_macro_level <= 0);
+ if (ImGui::ArrowButton("##macro_prev", ImGuiDir_Left))
+ --m_macro_level;
+ ImGui::EndDisabled();
+
+ ImGui::SameLine();
+
+ ImGui::BeginDisabled(m_macro_level >= max_level);
+ if (ImGui::ArrowButton("##macro_next", ImGuiDir_Right))
+ ++m_macro_level;
+ ImGui::EndDisabled();
+
+ ImGui::SameLine();
+
+ if (max_level > 0)
+ {
+ ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x -
+ ImGui::CalcTextSize("full reset").x -
+ ImGui::GetStyle().FramePadding.x * 6.0f -
+ ImGui::GetStyle().ItemSpacing.x * 2.0f);
+ ImGui::SliderInt("##macro_level", &m_macro_level, 0, max_level, "level %d");
+ }
+ else
+ {
+ ImGui::TextDisabled("nothing to expand here");
+ ImGui::SameLine();
+ }
+
+ ImGui::SameLine();
+
+ if (ImGui::SmallButton("full"))
+ m_macro_level = max_level;
+
+ ImGui::SameLine();
+
+ if (ImGui::SmallButton("reset"))
+ m_macro_level = 0;
+
+ // status: where we are, and whether the tail is truly the fixpoint
+ if (max_level == 0)
+ ImGui::TextDisabled("already fully expanded");
+ else if (m_macro_level == max_level && expansion.fully_expanded())
+ ImGui::TextDisabled("level %d of %d — fully expanded", m_macro_level, max_level);
+ else if (m_macro_level == max_level)
+ ImGui::TextDisabled("level %d — stopped at the expansion cap", m_macro_level);
+ else
+ ImGui::TextDisabled("level %d of %d", m_macro_level, max_level);
+
+ // which macros the next layer will unroll, so the step reads ahead
+ if (m_macro_level < max_level)
+ {
+ const std::vector<std::string>& next =
+ expansion.expanded[static_cast<size_t>(m_macro_level) + 1];
+
+ if (!next.empty())
+ {
+ std::string names;
+
+ for (const std::string& name : next)
+ {
+ if (!names.empty())
+ names += ", ";
+
+ names += name;
+ }
+
+ ImGui::SameLine();
+ ImGui::TextDisabled("| next: %s", names.c_str());
+ }
+ }
+
+ ImGui::Separator();
+
+ m_macro_output = expansion.levels[static_cast<size_t>(m_macro_level)];
+
+ ImGui::InputTextMultiline("##macro_output", &m_macro_output,
+ ImGui::GetContentRegionAvail(),
+ ImGuiInputTextFlags_ReadOnly);
+ }
+
+ ImGui::End();
+ }
+
auto Ui::push_console(std::string line) -> void
{
m_console_lines.push_back(std::move(line));
diff --git a/src/ui/ui.h b/src/ui/ui.h
index 5730059..70cf691 100644
--- a/src/ui/ui.h
+++ b/src/ui/ui.h
@@ -51,6 +51,8 @@ namespace Hsdbg
bool disassembly = true;
bool console = true;
bool profiler = true;
+ bool timeline = true;
+ bool macros = true;
bool demo = false;
};
@@ -75,6 +77,8 @@ namespace Hsdbg
auto draw_disassembly_panel(Debugger& debugger) -> void;
auto draw_console_panel(Debugger& debugger) -> void;
auto draw_profiler_panel(Debugger& debugger) -> void;
+ auto draw_timeline_panel(Debugger& debugger) -> void;
+ auto draw_macros_panel(Debugger& debugger) -> void;
auto push_console(std::string line) -> void;
auto report(const Result<void>& result, std::string_view action) -> void;
@@ -115,6 +119,12 @@ namespace Hsdbg
std::string m_source_filter;
std::string m_trace_input;
+ // macros panel: the invocation being unrolled, the current unroll depth,
+ // and the read-only buffer that shows the tokens at that depth
+ std::string m_macro_input;
+ std::string m_macro_output;
+ int m_macro_level = 0;
+
uint64_t m_followed_stop = 0;
std::filesystem::path m_followed_target;
@@ -123,6 +133,7 @@ namespace Hsdbg
bool m_console_scroll_pending = false;
bool m_load_target_pending = false;
bool m_focus_breakpoints = false;
+ bool m_focus_macros = false;
bool m_focus_symbols = false;
bool m_focus_disassembly = false;
bool m_scroll_to_program_counter = false;