///|
/// Message and term namespaces are distinct. Term names omit the leading '-'.
pub(all) enum SymbolKind {
MessageSymbol
TermSymbol
} derive(Eq)
///|
pub(all) struct Definition {
kind : SymbolKind
name : String
node : Node
attributes : Array[String]
has_value : Bool
}
///|
/// Only message/term references are resolution targets; variables and functions
/// are external runtime inputs, and are deliberately excluded.
pub(all) struct Reference {
kind : SymbolKind
name : String
attribute : String?
span : Span
} derive(Eq)
///|
pub(all) struct DuplicateDefinition {
kind : SymbolKind
name : String
first_span : Span
duplicate_span : Span
} derive(Eq)
///|
pub(all) enum MissingReferenceReason {
UnknownDefinition
UnknownAttribute
MissingValue
} derive(Eq)
///|
pub(all) struct MissingReference {
reference : Reference
reason : MissingReferenceReason
} derive(Eq)
///|
pub(all) enum CatalogError {
Traversal(TreeError)
ExpectedResource
InvalidIdentifier(String)
MalformedNode(String, Span)
JunkPresent(Span)
MissingDefinition(SymbolKind, String)
AmbiguousDefinition(SymbolKind, String)
RenameConflict(SymbolKind, String)
} derive(Eq)
///|
/// Immutable index snapshot. Returned arrays are copies of index storage.
/// Node payloads are shared, so callers must not mutate AST arrays after indexing.
pub struct Catalog {
entries : Array[Definition]
uses : Array[Reference]
repeated : Array[DuplicateDefinition]
junk : Array[Span]
messages : Map[String, Array[Int32]]
terms : Map[String, Array[Int32]]
}
///|
/// Fluent identifier syntax, without the term sigil. ASCII letters start an id;
/// subsequent characters may also be digits, '_' or '-'.
pub fn valid_symbol_id(name : String) -> Bool {
let chars = name.to_array()
if chars.is_empty() {
return false
}
fn letter(c : Char) -> Bool {
(c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
}
if !letter(chars[0]) {
return false
}
for c in chars {
if !letter(c) && !(c >= '0' && c <= '9') && c != '_' && c != '-' {
return false
}
}
true
}
///|
fn catalog_identifier(node : Node) -> Result[String, CatalogError] {
match node.kind {
Identifier(name) =>
if valid_symbol_id(name) {
Ok(name)
} else {
Err(InvalidIdentifier(name))
}
_ => Err(MalformedNode("expected Identifier", node.span))
}
}
///|
fn catalog_attribute(node : Node?) -> Result[String?, CatalogError] {
match node {
None => Ok(None)
Some(n) =>
match catalog_identifier(n) {
Ok(name) => Ok(Some(name))
Err(error) => Err(error)
}
}
}
///|
/// Build a source-ordered index, preserving all duplicate definitions rather
/// than silently choosing a winner. Junk is recorded, not discarded. Definitions
/// must be Resource children; nested definitions/resources are rejected.
pub fn Catalog::build(
resource : Node,
max_depth~ : Int32 = 256,
max_nodes~ : Int32 = 100000,
) -> Result[Catalog, CatalogError] {
guard resource.kind is Resource(_) else { return Err(ExpectedResource) }
let visits = match resource.walk(max_depth~, max_nodes~) {
Ok(v) => v
Err(e) => return Err(Traversal(e))
}
let entries : Array[Definition] = []
let uses : Array[Reference] = []
let repeated : Array[DuplicateDefinition] = []
let junk : Array[Span] = []
let messages : Map[String, Array[Int32]] = Map([])
let terms : Map[String, Array[Int32]] = Map([])
for visit in visits {
let node = visit.node
let definition = match node.kind {
Message(id, value, attrs, _) =>
Some((MessageSymbol, id, attrs, value is Some(_)))
Term(id, _, attrs, _) => Some((TermSymbol, id, attrs, true))
_ => None
}
if definition is Some((kind, id, attrs, has_value)) {
if visit.depth != 1 {
return Err(MalformedNode("definition outside Resource body", node.span))
}
let name = match catalog_identifier(id) {
Ok(name) => name
Err(e) => return Err(e)
}
let attributes : Array[String] = []
for attr in attrs {
guard attr.kind is Attribute(attr_id, _) else {
return Err(MalformedNode("expected Attribute", attr.span))
}
match catalog_identifier(attr_id) {
Ok(name) => attributes.push(name)
Err(e) => return Err(e)
}
}
let index = match kind {
MessageSymbol => messages
TermSymbol => terms
}
let positions = match index.get(name) {
Some(positions) => {
repeated.push({
kind,
name,
first_span: entries[positions[0]].node.span,
duplicate_span: node.span,
})
positions
}
None => {
let positions : Array[Int32] = []
index[name] = positions
positions
}
}
positions.push(entries.length())
entries.push({ kind, name, node, attributes, has_value, })
}
match node.kind {
MessageReference(id, attr) | TermReference(id, attr, _) => {
let kind = match node.kind {
TermReference(_, _, _) => TermSymbol
_ => MessageSymbol
}
let name = match catalog_identifier(id) {
Ok(name) => name
Err(e) => return Err(e)
}
let attribute = match catalog_attribute(attr) {
Ok(a) => a
Err(e) => return Err(e)
}
uses.push({ kind, name, attribute, span: node.span, })
}
Junk(_, _) => junk.push(node.span)
Resource(_) =>
if visit.depth != 0 {
return Err(MalformedNode("nested Resource", node.span))
}
_ => ()
}
}
Ok({ entries, uses, repeated, junk, messages, terms, })
}
///|
pub fn Catalog::definitions(self : Catalog) -> Array[Definition] {
// Copy attribute arrays too; node payloads remain shared as documented.
self.entries.map(d => { ..d, attributes: d.attributes.map(a => a), })
}
///|
pub fn Catalog::references(self : Catalog) -> Array[Reference] {
self.uses.map(r => r)
}
///|
pub fn Catalog::duplicates(self : Catalog) -> Array[DuplicateDefinition] {
self.repeated.map(d => d)
}
///|
pub fn Catalog::junk_spans(self : Catalog) -> Array[Span] {
self.junk.map(s => s)
}
///|
/// Exact namespace lookup; returns every match in source order, including duplicates.
pub fn Catalog::find(
self : Catalog,
kind : SymbolKind,
name : String,
) -> Array[Definition] {
let index = match kind {
MessageSymbol => self.messages
TermSymbol => self.terms
}
match index.get(name) {
Some(positions) =>
positions.map(i => {
let d = self.entries[i]
{ ..d, attributes: d.attributes.map(a => a), }
})
None => []
}
}
///|
/// Resolve against this resource only. Attribute access requires that attribute;
/// bare access requires a value. With duplicates, any matching definition suffices;
/// ambiguity is reported separately by duplicates(). Junk may hide further uses.
pub fn Catalog::missing_references(self : Catalog) -> Array[MissingReference] {
let result : Array[MissingReference] = []
for reference in self.uses {
let targets = self.find(reference.kind, reference.name)
if targets.is_empty() {
result.push({ reference, reason: UnknownDefinition, })
} else {
let mut found = false
for target in targets {
match reference.attribute {
Some(attr) => if target.attributes.contains(attr) { found = true }
None => if target.has_value { found = true }
}
}
if !found {
result.push({
reference,
reason: match reference.attribute {
Some(_) => UnknownAttribute
None => MissingValue
},
})
}
}
}
result
}
///|
fn catalog_renamed_id(id : Node, old_name : String, new_name : String) -> Node {
match id.kind {
Identifier(name) if name == old_name =>
{ kind: Identifier(new_name), span: id.span, }
_ => id
}
}
///|
/// Transactional, syntax-aware rename of one definition and all matching
/// references, including attributes and nested term call arguments. Text, comments,
/// variables, function names and the other namespace are untouched. Conflicts,
/// invalid ids, missing/ambiguous sources, traversal limits and any Junk fail
/// before edits. An identity rename still validates input. Spans remain source
/// provenance, not recalculated offsets for a future serialization.
pub fn Node::rename_symbol(
self : Node,
kind : SymbolKind,
old_name : String,
new_name : String,
max_depth~ : Int32 = 256,
max_nodes~ : Int32 = 100000,
) -> Result[Node, CatalogError] {
if !valid_symbol_id(old_name) {
return Err(InvalidIdentifier(old_name))
}
if !valid_symbol_id(new_name) {
return Err(InvalidIdentifier(new_name))
}
let catalog = match Catalog::build(self, max_depth~, max_nodes~) {
Ok(c) => c
Err(e) => return Err(e)
}
if !catalog.junk.is_empty() {
return Err(JunkPresent(catalog.junk[0]))
}
let source = catalog.find(kind, old_name)
if source.is_empty() {
return Err(MissingDefinition(kind, old_name))
}
if source.length() != 1 {
return Err(AmbiguousDefinition(kind, old_name))
}
if old_name != new_name && !catalog.find(kind, new_name).is_empty() {
return Err(RenameConflict(kind, new_name))
}
self
.transform(
n => {
let renamed = match n.kind {
Message(id, value, attrs, comment) if kind == MessageSymbol =>
Message(
catalog_renamed_id(id, old_name, new_name),
value,
attrs,
comment,
)
Term(id, value, attrs, comment) if kind == TermSymbol =>
Term(
catalog_renamed_id(id, old_name, new_name),
value,
attrs,
comment,
)
MessageReference(id, attr) if kind == MessageSymbol =>
MessageReference(catalog_renamed_id(id, old_name, new_name), attr)
TermReference(id, attr, args) if kind == TermSymbol =>
TermReference(catalog_renamed_id(id, old_name, new_name), attr, args)
other => other
}
{ kind: renamed, span: n.span, }
},
max_depth~,
max_nodes~,
)
.map_err(e => Traversal(e))
}
///|
pub extend SymbolKind with Eq::{equal, not_equal}
///|
pub extend Reference with Eq::{equal, not_equal}
///|
pub extend DuplicateDefinition with Eq::{equal, not_equal}
///|
pub extend MissingReferenceReason with Eq::{equal, not_equal}
///|
pub extend MissingReference with Eq::{equal, not_equal}
///|
pub extend CatalogError with Eq::{equal, not_equal}