-
Notifications
You must be signed in to change notification settings - Fork 1
feat: debug conflict resolution graph printing (#150) #301
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -104,6 +104,32 @@ private[parser] object ConflictResolutionTable: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for node <- table.keys do loop(Action.Enter(node) :: Nil) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def toMermaid(using Log): String = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val sb = new StringBuilder | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sb.append("graph TD\n") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+124
to
+126
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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") |
Copilot
AI
Mar 4, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 |
Copilot
AI
Mar 4, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
toMermaidis new behavior and is now used to emit a debug artifact fromcreateTablesImpl, 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 intest/src/alpaca/internal/parserthat builds a smallConflictResolutionTableand asserts key nodes/edges/escaping would help prevent regressions.