///|
fn forest_query_budget(
work_limit : Int32,
) -> Result[ForestAnalysisBudget, Problem] {
if work_limit <= 0 {
Err(InvalidLimit("work_limit"))
} else {
Ok({ name: "query_work", limit: work_limit, used: 0, })
}
}
///|
/// One queue implementation supports both structural and viable reachability.
/// Token leaves are terminal even if a malformed internal fixture adds families.
fn forest_query_walk(
forest : Forest,
root : Int32?,
productive : Array[Bool]?,
budget : ForestAnalysisBudget,
) -> Array[Int32] raise ForestAnalysisHalt {
let root = match root {
None => return []
Some(id) => id
}
forest_analysis_check_id(forest, root)
match productive {
Some(flags) => if !flags[root] { return [] }
None => ()
}
let seen = Array::make(forest.nodes.length(), false)
let queue = [root]
seen[root] = true
let mut cursor = 0
while cursor < queue.length() {
budget.tick()
let id = queue[cursor]
cursor += 1
match forest.nodes[id].kind {
TokenLeaf(_) => ()
_ =>
for family in forest.nodes[id].families {
let viable = match productive {
None => {
budget.tick()
true
}
Some(flags) =>
forest_analysis_family_productive(forest, family, flags, budget)
}
if viable {
for child in family.children {
budget.tick()
forest_analysis_check_id(forest, child)
if !seen[child] {
seen[child] = true
queue.push(child)
}
}
}
}
}
}
queue.sort()
queue
}
///|
/// All matching IDs in allocation order; namespaces remain distinct.
pub fn Forest::nodes_of_kind(self : Forest, kind : NodeKind) -> Array[Int32] {
let ids : Array[Int32] = []
for id, node in self.nodes {
if node.kind == kind {
ids.push(id)
}
}
ids
}
///|
/// Search every stored symbol node, including disconnected chart material.
pub fn Forest::symbol_nodes(self : Forest, name : String) -> Array[Int32] {
self.nodes_of_kind(SymbolNode(name))
}
///|
fn forest_query_span(
forest : Forest,
start : Int32,
end : Int32,
) -> Result[Unit, Problem] {
if start < 0 || end < start || end > forest.tokens.length() {
Err(Syntax(start, "invalid token-index span"))
} else {
Ok(())
}
}
///|
/// Exact half-open token interval, not the tokens' caller-defined source offsets.
/// Zero-width intervals are valid, including at the end of input.
pub fn Forest::nodes_at_span(
self : Forest,
start : Int32,
end : Int32,
) -> Result[Array[Int32], Problem] {
match forest_query_span(self, start, end) {
Err(problem) => return Err(problem)
Ok(_) => ()
}
let ids : Array[Int32] = []
for id, node in self.nodes {
if node.start == start && node.end == end {
ids.push(id)
}
}
Ok(ids)
}
///|
pub fn Forest::symbol_nodes_at(
self : Forest,
name : String,
start : Int32,
end : Int32,
) -> Result[Array[Int32], Problem] {
match self.nodes_at_span(start, end) {
Err(problem) => Err(problem)
Ok(ids) =>
Ok(ids.filter(fn(id) { self.nodes[id].kind == SymbolNode(name) }))
}
}
///|
/// Follow all stored families from the accepted root, cycle-safely.
/// A rejected forest has no root and therefore no root-reachable IDs.
pub fn Forest::reachable_nodes(
self : Forest,
work_limit~ : Int32 = 1000000,
) -> Result[Array[Int32], Problem] {
self.query_reachable(self.root, work_limit)
}
///|
/// Useful for chart fragments and rejected reports that have no accepted root.
pub fn Forest::reachable_from(
self : Forest,
id : Int32,
work_limit~ : Int32 = 1000000,
) -> Result[Array[Int32], Problem] {
self.query_reachable(Some(id), work_limit)
}
///|
fn Forest::query_reachable(
self : Forest,
root : Int32?,
work_limit : Int32,
) -> Result[Array[Int32], Problem] {
let budget = match forest_query_budget(work_limit) {
Err(problem) => return Err(problem)
Ok(budget) => budget
}
Ok(forest_query_walk(self, root, None, budget)) catch {
ForestAnalysisHalt(problem) => Err(problem)
}
}
///|
/// Finite-proof productivity over every node, independent of root reachability.
pub fn Forest::productive_nodes(
self : Forest,
work_limit~ : Int32 = 1000000,
) -> Result[Array[Int32], Problem] {
let budget = match forest_query_budget(work_limit) {
Err(problem) => return Err(problem)
Ok(budget) => budget
}
try {
let productive = forest_analysis_productive(self, budget)
let ids : Array[Int32] = []
for id, flag in productive {
budget.tick()
if flag {
ids.push(id)
}
}
Ok(ids)
} catch {
ForestAnalysisHalt(problem) => Err(problem)
}
}
///|
/// Local ambiguity means at least two productive packed families at a node.
/// Parents with only one family are not listed merely because a child is
/// ambiguous. By default consider only viable root-reachable nodes; false
/// includes disconnected productive nodes. Token leaves are never ambiguous.
pub fn Forest::ambiguity_nodes(
self : Forest,
reachable_only~ : Bool = true,
work_limit~ : Int32 = 1000000,
) -> Result[Array[Int32], Problem] {
let budget = match forest_query_budget(work_limit) {
Err(problem) => return Err(problem)
Ok(budget) => budget
}
try {
let productive = forest_analysis_productive(self, budget)
let candidates = if reachable_only {
forest_query_walk(self, self.root, Some(productive), budget)
} else {
Array::makei(self.nodes.length(), fn(id) { id })
}
let ids : Array[Int32] = []
for id in candidates {
budget.tick()
match self.nodes[id].kind {
TokenLeaf(_) => ()
_ => {
let mut alternatives = 0
for family in self.nodes[id].families {
if forest_analysis_family_productive(
self, family, productive, budget,
) {
alternatives += 1
if alternatives == 2 {
ids.push(id)
break
}
}
}
}
}
}
Ok(ids)
} catch {
ForestAnalysisHalt(problem) => Err(problem)
}
}
///|
/// Quote all user strings for a DOT quoted label, never an HTML label or ID.
/// Also escape backslashes so Graphviz substitutions like \N cannot activate.
/// Rare C0/DEL controls become visible text instead of raw control bytes.
fn forest_query_dot_escape(
value : String,
budget : ForestAnalysisBudget,
) -> String raise ForestAnalysisHalt {
let out = StringBuilder()
for c in value.iter() {
budget.tick()
match c {
'\\' => out.write_string("\\\\")
'"' => out.write_string("\\\"")
'\n' => out.write_string("\\n")
'\r' => out.write_string("\\r")
'\t' => out.write_string("\\t")
_ => {
let code = c.to_int()
if code < 32 || code == 127 {
out.write_string("\\\\control(\{code})")
} else {
out.write_char(c)
}
}
}
}
out.to_string()
}
///|
fn forest_query_rule_tag(
forest : Forest,
rule : Int32,
) -> String raise ForestAnalysisHalt {
if rule < 0 || rule >= forest.rules.length() {
raise ForestAnalysisHalt(
InvalidGrammar("rule index outside forest snapshot"),
)
}
forest.rules[rule].label
}
///|
fn forest_query_node_label(
forest : Forest,
id : Int32,
) -> String raise ForestAnalysisHalt {
let node = forest.nodes[id]
let span = "[\{node.start},\{node.end})"
match node.kind {
SymbolNode(name) => "\{name} \{span}"
Intermediate(rule, dot) => {
let tag = forest_query_rule_tag(forest, rule)
"prefix #\{rule} \{tag} ยท\{dot} \{span}"
}
TokenLeaf(index) => {
if index < 0 || index >= forest.tokens.length() {
raise ForestAnalysisHalt(
InvalidToken(index, "index outside forest token snapshot"),
)
}
let token = forest.tokens[index]
"token #\{index} \{token.kind}: \{token.text} \{span}"
}
}
}
///|
/// Deterministic DOT encodes AND/OR structure with a separate box per family.
/// Ordered child edges keep their positions, including repeated child IDs.
/// Empty families remain visible. Cycles are exported without tree expansion.
/// Default output covers all structural root-reachable families; false includes
/// the entire chart snapshot. Work also charges every escaped label character.
pub fn Forest::to_dot(
self : Forest,
reachable_only~ : Bool = true,
work_limit~ : Int32 = 1000000,
) -> Result[String, Problem] {
let budget = match forest_query_budget(work_limit) {
Err(problem) => return Err(problem)
Ok(budget) => budget
}
try {
let ids = if reachable_only {
forest_query_walk(self, self.root, None, budget)
} else {
Array::makei(self.nodes.length(), fn(id) { id })
}
let out = StringBuilder()
out.write_string("digraph Forest {\n rankdir=LR;\n")
for id in ids {
budget.tick()
let node = self.nodes[id]
let label = forest_query_dot_escape(
forest_query_node_label(self, id),
budget,
)
let shape = match node.kind {
TokenLeaf(_) => "box"
_ => "ellipse"
}
let root_style = if self.root == Some(id) {
", peripheries=2"
} else {
""
}
out.write_string(
" n\{id} [shape=\{shape}\{root_style}, label=\"\{label}\"];\n",
)
match node.kind {
TokenLeaf(_) => ()
_ =>
for index, family in node.families {
budget.tick()
let tag = forest_query_rule_tag(self, family.rule)
let label = forest_query_dot_escape(
"#\{family.rule}: \{tag}",
budget,
)
out.write_string(
" f\{id}_\{index} [shape=box, label=\"\{label}\"];\n",
)
out.write_string(" n\{id} -> f\{id}_\{index};\n")
for position, child in family.children {
budget.tick()
forest_analysis_check_id(self, child)
out.write_string(
" f\{id}_\{index} -> n\{child} [label=\"\{position}\"];\n",
)
}
}
}
}
out.write_string("}\n")
Ok(out.to_string())
} catch {
ForestAnalysisHalt(problem) => Err(problem)
}
}