feat: debug conflict resolution graph printing (#150)#301
feat: debug conflict resolution graph printing (#150)#301halotukozak wants to merge 1 commit intomasterfrom
Conversation
🏃 Runtime Benchmark
|
There was a problem hiding this comment.
Pull request overview
Adds Mermaid graph output for the conflict resolution precedence graph to help debug/visualize conflict resolution rules (Issue #150).
Changes:
- Emit a new
conflictResolutions.mmddebug file during table creation. - Add
ConflictResolutionTable.toMermaidto render the precedence graph in Mermaid format.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
src/alpaca/internal/parser/createTables.scala |
Writes Mermaid debug output alongside the existing conflict resolution debug table output. |
src/alpaca/internal/parser/ConflictResolution.scala |
Implements Mermaid rendering for the conflict resolution graph. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def toMermaid(using Log): String = | ||
| val sb = new StringBuilder | ||
| sb.append("graph TD\n") |
There was a problem hiding this comment.
toMermaid is new behavior and is now used to emit a debug artifact from createTablesImpl, but there’s no unit test coverage to ensure the output remains valid Mermaid (especially for token/production names with special characters) and stable over time. Adding a focused test in test/src/alpaca/internal/parser that builds a small ConflictResolutionTable and asserts key nodes/edges/escaping would help prevent regressions.
| def nodeName(key: ConflictKey): String = | ||
| val raw = key match | ||
| case p: Production => p.name match | ||
| case null => show"$p" | ||
| case name: String => name | ||
| case s: String => show"Token($s)" | ||
| // Escape special chars for mermaid | ||
| raw.replace(" ", "_").replace("(", "[").replace(")", "]").replace("->", "_to_").replace("ε", "epsilon") | ||
|
|
||
| def nodeLabel(key: ConflictKey): String = key match | ||
| case p: Production => show"$p" | ||
| case s: String => show"Token($s)" | ||
|
|
||
| val nodes = (table.keySet ++ table.values.flatten).toSet | ||
| for node <- nodes do | ||
| sb.append(s" ${nodeName(node)}[\"${nodeLabel(node)}\"]\n") | ||
|
|
||
| for (from, toSet) <- table; to <- toSet do | ||
| sb.append(s" ${nodeName(from)} --> ${nodeName(to)}\n") |
There was a problem hiding this comment.
nodeName is used as the Mermaid node identifier, but the current “escaping” can still produce invalid IDs and collisions (e.g., replacing ( with [/] introduces Mermaid syntax chars; token/production names can contain many other non-identifier characters; production names can collide with token-derived names). Consider generating a safe, unique ID per ConflictKey (e.g., P_/T_ prefix + stable hash or an indexed map) and keep the human-readable text only in the label.
| def nodeName(key: ConflictKey): String = | |
| val raw = key match | |
| case p: Production => p.name match | |
| case null => show"$p" | |
| case name: String => name | |
| case s: String => show"Token($s)" | |
| // Escape special chars for mermaid | |
| raw.replace(" ", "_").replace("(", "[").replace(")", "]").replace("->", "_to_").replace("ε", "epsilon") | |
| def nodeLabel(key: ConflictKey): String = key match | |
| case p: Production => show"$p" | |
| case s: String => show"Token($s)" | |
| val nodes = (table.keySet ++ table.values.flatten).toSet | |
| for node <- nodes do | |
| sb.append(s" ${nodeName(node)}[\"${nodeLabel(node)}\"]\n") | |
| for (from, toSet) <- table; to <- toSet do | |
| sb.append(s" ${nodeName(from)} --> ${nodeName(to)}\n") | |
| // Generate safe, unique Mermaid node identifiers per ConflictKey. | |
| val idMap = mutable.LinkedHashMap.empty[ConflictKey, String] | |
| var prodIdx = 0 | |
| var tokIdx = 0 | |
| var otherIdx = 0 | |
| def nodeId(key: ConflictKey): String = | |
| idMap.getOrElseUpdate( | |
| key, | |
| key match | |
| case _: Production => | |
| prodIdx += 1 | |
| s"P_$prodIdx" | |
| case _: String => | |
| tokIdx += 1 | |
| s"T_$tokIdx" | |
| case _ => | |
| otherIdx += 1 | |
| s"N_$otherIdx" | |
| ) | |
| def nodeLabel(key: ConflictKey): String = key match | |
| case p: Production => show"$p" | |
| case s: String => show"Token($s)" | |
| val nodes = (table.keySet ++ table.values.flatten).toSet | |
| for node <- nodes do | |
| sb.append(s" ${nodeId(node)}[\"${nodeLabel(node)}\"]\n") | |
| for (from, toSet) <- table; to <- toSet do | |
| sb.append(s" ${nodeId(from)} --> ${nodeId(to)}\n") |
| val nodes = (table.keySet ++ table.values.flatten).toSet | ||
| for node <- nodes do | ||
| sb.append(s" ${nodeName(node)}[\"${nodeLabel(node)}\"]\n") |
There was a problem hiding this comment.
nodeLabel is injected into [...] with double quotes, but the label text isn’t escaped. Since token names (and production names) can include characters like ", ], or newlines, this can generate invalid Mermaid. Escape label content (at least \, ", and newlines) before writing it, or use a safer encoding for labels.
| val nodes = (table.keySet ++ table.values.flatten).toSet | |
| for node <- nodes do | |
| sb.append(s" ${nodeName(node)}[\"${nodeLabel(node)}\"]\n") | |
| def escapeLabel(label: String): String = | |
| label | |
| .replace("\\", "\\\\") | |
| .replace("\"", "\\\"") | |
| .replace("\n", "\\n") | |
| .replace("\r", "\\r") | |
| val nodes = (table.keySet ++ table.values.flatten).toSet | |
| for node <- nodes do | |
| sb.append(s" ${nodeName(node)}[\"${escapeLabel(nodeLabel(node))}\"]\n") |
| for node <- nodes do | ||
| sb.append(s" ${nodeName(node)}[\"${nodeLabel(node)}\"]\n") | ||
|
|
||
| for (from, toSet) <- table; to <- toSet do |
There was a problem hiding this comment.
The Mermaid output iteration order is not deterministic (nodes is a Set and table is a Map), so the .mmd file may change between runs even when the underlying graph is the same. For debug artifacts that are intended to be diffed, consider sorting nodes/edges (e.g., by stable ID or label) before appending to the StringBuilder.
| for node <- nodes do | |
| sb.append(s" ${nodeName(node)}[\"${nodeLabel(node)}\"]\n") | |
| for (from, toSet) <- table; to <- toSet do | |
| // Sort nodes to ensure deterministic Mermaid output | |
| for node <- nodes.toList.sortBy(nodeName) do | |
| sb.append(s" ${nodeName(node)}[\"${nodeLabel(node)}\"]\n") | |
| // Sort edges (both sources and targets) for deterministic ordering | |
| for | |
| (from, toSet) <- table.toList.sortBy { case (fromKey, _) => nodeName(fromKey) } | |
| to <- toSet.toList.sortBy(nodeName) | |
| do |
Codecov Report❌ Patch coverage is
@@ Coverage Diff @@
## master #301 +/- ##
=========================================
Coverage ? 40.35%
=========================================
Files ? 35
Lines ? 451
Branches ? 0
=========================================
Hits ? 182
Misses ? 269
Partials ? 0
🚀 New features to boost your workflow:
|
📊 Test Compilation Benchmark
Result: Current branch is 0.903s faster (1.73%) ✅ |
Summary
🤖 Generated with Claude Code