Skip to main content

morphir_core/
node_address.rs

1//! Draft, storage-independent addresses for semantic IR nodes.
2//!
3//! The authority `ir` deliberately differs from the V4 document-tree `pkg`
4//! authority. A node URI identifies a node in a normalized distribution, not
5//! the file that happened to contain it.
6
7pub use crate::format_version::ReleaseTriplet as IrFormatVersion;
8use crate::ir::v4::serde_v4::with_fingerprint_semantics;
9use crate::ir::v4::{TypeEncoding, with_type_encoding};
10use crate::naming::{Name, PackageName, Path};
11use serde::Serialize;
12use sha2::{Digest, Sha256};
13use std::fmt;
14
15mod index;
16mod legacy_v3;
17pub use index::{IndexedNodeKind, NodeCatalog, NodeIndex, NodeResolutionError, ResolvedNode};
18pub use legacy_v3::{LegacyNodeIdError, convert_v3_node_id};
19
20/// A syntactically invalid semantic node URI.
21#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
22pub enum NodeUriError {
23    /// An unpinned positional path can silently retarget without a guard.
24    #[error("an unpinned indexed node address requires a guard")]
25    MissingGuard,
26    /// The input cannot be interpreted as one canonical node address.
27    #[error("invalid node URI: {0}")]
28    Invalid(String),
29    /// A caller tried to fingerprint a non-positional step or an unserializable node.
30    #[error("cannot fingerprint selected semantic child: {0}")]
31    InvalidFingerprint(String),
32}
33
34/// The artifact selected by the caller's resolution context.
35#[derive(Debug, Clone, PartialEq, Eq, Hash)]
36pub enum ArtifactSelector {
37    /// One named package, possibly ambiguous until the context chooses a release.
38    Package(PackageName),
39    /// A caller-provided current working artifact alias.
40    Workspace(String),
41}
42
43/// The package containing a module inside a distribution.
44#[derive(Debug, Clone, PartialEq, Eq, Hash)]
45pub enum NodeOwner {
46    OwnPackage,
47    Dependency(PackageName),
48}
49
50/// The first semantic node selected within a distribution.
51#[derive(Debug, Clone, PartialEq, Eq, Hash)]
52pub enum NodeRoot {
53    Distribution,
54    Package,
55    Dependency(PackageName),
56    EntryPoint(String),
57    Module {
58        owner: NodeOwner,
59        module: Path,
60    },
61    Type {
62        owner: NodeOwner,
63        module: Path,
64        name: Name,
65    },
66    Value {
67        owner: NodeOwner,
68        module: Path,
69        name: Name,
70    },
71}
72
73/// One typed child selection. Variants do not carry irrelevant names or indices.
74#[derive(Debug, Clone, PartialEq, Eq, Hash)]
75pub enum NodeStep {
76    TypeExpression,
77    Body,
78    ValueInputType(Name),
79    ValueInputAnnotation(Name),
80    ValueOutputType,
81    DerivedBaseType,
82    PartialTypeExpression,
83    CustomConstructor(Name),
84    ConstructorArgument(usize),
85    RecordField(Name),
86    ExtensibleRecordField(Name),
87    TypeFunctionParameter,
88    TypeFunctionResult,
89    ReferenceArgument(usize),
90    ApplyFunction,
91    ApplyArgument,
92    FieldSubject,
93    DestructurePattern,
94    DestructureValue,
95    DestructureBody,
96    IfCondition,
97    IfThen,
98    IfElse,
99    LambdaPattern,
100    LambdaBody,
101    LetDefinition(Name),
102    LetBody,
103    ListElement(usize),
104    PatternMatchSubject,
105    TupleElement(usize),
106    PatternMatchCasePattern(usize),
107    PatternMatchCaseBody(usize),
108    UpdateSubject,
109    UpdateField(Name),
110    AsPatternChild,
111    PatternTupleElement(usize),
112    PatternConstructorArgument(usize),
113    HeadTailHead,
114    HeadTailTail,
115    ExternalFallback,
116    IncompletePartialBody,
117    HoleExpectedType,
118    /// The semantic Type stored in a Value or Pattern's inferredType attribute.
119    InferredType,
120    /// An ordered annotation entry on a specification.
121    AnnotationEntry(usize),
122    /// An ordered Value argument within a structured annotation.
123    AnnotationArgument(usize),
124}
125
126impl NodeStep {
127    /// Whether this step selects an ordered child whose identity needs a guard.
128    pub fn is_positional(&self) -> bool {
129        matches!(
130            self,
131            Self::ConstructorArgument(_)
132                | Self::AnnotationEntry(_)
133                | Self::AnnotationArgument(_)
134                | Self::ReferenceArgument(_)
135                | Self::ListElement(_)
136                | Self::TupleElement(_)
137                | Self::PatternMatchCasePattern(_)
138                | Self::PatternMatchCaseBody(_)
139                | Self::PatternTupleElement(_)
140                | Self::PatternConstructorArgument(_)
141        )
142    }
143}
144
145/// Current resolution or a verified immutable artifact snapshot.
146#[derive(Debug, Clone, PartialEq, Eq, Hash)]
147pub enum ArtifactRevision {
148    Current,
149    Pinned(Sha256Digest),
150}
151
152/// A lowercase SHA-256 digest in canonical hexadecimal form.
153#[derive(Debug, Clone, PartialEq, Eq, Hash)]
154pub struct Sha256Digest(String);
155
156impl Sha256Digest {
157    /// Digest the exact bytes of an acquired immutable snapshot.
158    pub fn from_bytes(bytes: &[u8]) -> Self {
159        Self(
160            Sha256::digest(bytes)
161                .iter()
162                .map(|byte| format!("{byte:02x}"))
163                .collect(),
164        )
165    }
166
167    pub fn parse(value: &str) -> Result<Self, NodeUriError> {
168        let Some(hex) = value.strip_prefix("sha256:") else {
169            return Err(NodeUriError::Invalid("digest must use sha256".into()));
170        };
171        if hex.len() != 64
172            || !hex
173                .bytes()
174                .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
175        {
176            return Err(NodeUriError::Invalid(
177                "digest must have 64 lowercase hex digits".into(),
178            ));
179        }
180        Ok(Self(hex.to_owned()))
181    }
182}
183
184impl fmt::Display for Sha256Digest {
185    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186        write!(f, "sha256:{}", self.0)
187    }
188}
189
190/// Computes one guard from the ordered child selections on a semantic path.
191///
192/// Call `push` once for each ordered step, from root to leaf. The selected
193/// child is a typed IR node, serialized through the normalized model; its
194/// source JSON/YAML syntax and document-tree file location never enter the
195/// digest. Named ancestors and unrelated siblings are deliberately excluded.
196#[derive(Clone, Copy)]
197enum FingerprintMode {
198    Legacy,
199    V4_1,
200}
201
202#[derive(Clone)]
203pub struct NodeFingerprintBuilder {
204    hash: Sha256,
205    mode: FingerprintMode,
206}
207
208impl Default for NodeFingerprintBuilder {
209    fn default() -> Self {
210        Self::new()
211    }
212}
213
214impl NodeFingerprintBuilder {
215    pub fn new() -> Self {
216        let mut hash = Sha256::new();
217        hash.update(b"morphir-node-fingerprint-draft.1\0");
218        Self {
219            hash,
220            mode: FingerprintMode::Legacy,
221        }
222    }
223
224    /// Build a guard for V4.1 nodes, omitting nonsemantic attributes before
225    /// the V4 serializer chooses its shorthand or expanded representation.
226    pub fn for_v4_1() -> Self {
227        Self {
228            mode: FingerprintMode::V4_1,
229            ..Self::new()
230        }
231    }
232
233    /// Add one selected ordered child and its typed semantic subtree.
234    pub fn push<T: Serialize>(&mut self, step: &NodeStep, child: &T) -> Result<(), NodeUriError> {
235        let (role, index) = match step {
236            NodeStep::ConstructorArgument(index) => ("constructor/argument", *index),
237            NodeStep::AnnotationEntry(index) => ("annotation/entry", *index),
238            NodeStep::AnnotationArgument(index) => ("annotation/argument", *index),
239            NodeStep::ReferenceArgument(index) => ("reference/argument", *index),
240            NodeStep::ListElement(index) => ("list/element", *index),
241            NodeStep::TupleElement(index) => ("tuple/element", *index),
242            NodeStep::PatternMatchCasePattern(index) => ("pattern-match/case/pattern", *index),
243            NodeStep::PatternMatchCaseBody(index) => ("pattern-match/case/body", *index),
244            NodeStep::PatternTupleElement(index) => ("tuple-pattern/element", *index),
245            NodeStep::PatternConstructorArgument(index) => ("constructor-pattern/argument", *index),
246            _ => {
247                return Err(NodeUriError::InvalidFingerprint(
248                    "step is not positional".into(),
249                ));
250            }
251        };
252        let mut value = match self.mode {
253            FingerprintMode::Legacy => semantic_json(child),
254            FingerprintMode::V4_1 => with_fingerprint_semantics(|| semantic_json(child)),
255        }
256        .map_err(|error| NodeUriError::InvalidFingerprint(error.to_string()))?;
257        if matches!(self.mode, FingerprintMode::Legacy) {
258            strip_nonsemantic_attributes(&mut value);
259        }
260        let mut canonical = Vec::new();
261        write_canonical_json(&value, &mut canonical)
262            .map_err(|error| NodeUriError::InvalidFingerprint(error.to_string()))?;
263        self.hash.update((role.len() as u32).to_be_bytes());
264        self.hash.update(role.as_bytes());
265        self.hash.update((index as u64).to_be_bytes());
266        self.hash.update((canonical.len() as u64).to_be_bytes());
267        self.hash.update(&canonical);
268        Ok(())
269    }
270
271    /// Finish and return the lowercase SHA-256 token used in `guard=`.
272    pub fn finish(self) -> Sha256Digest {
273        Sha256Digest(
274            self.hash
275                .finalize()
276                .iter()
277                .map(|byte| format!("{byte:02x}"))
278                .collect(),
279        )
280    }
281}
282
283/// Choose one V4 type spelling even when a caller is serializing a document
284/// under a different thread-local profile at the same time.
285pub(crate) fn semantic_json<T: Serialize>(
286    node: &T,
287) -> Result<serde_json::Value, serde_json::Error> {
288    with_type_encoding(TypeEncoding::Expanded, || serde_json::to_value(node))
289}
290
291// Source coordinates and tool extensions locate or annotate a semantic node;
292// they cannot change the identity of a selected ordered child. Keep the
293// remaining attributes, including constraints and inferred types.
294fn strip_nonsemantic_attributes(value: &mut serde_json::Value) {
295    match value {
296        serde_json::Value::Array(items) => {
297            for item in items {
298                strip_nonsemantic_attributes(item);
299            }
300        }
301        serde_json::Value::Object(members) => {
302            // A document literal contains arbitrary user JSON. Its own
303            // `attributes` keys are data, not Morphir node metadata.
304            if members.contains_key("DocumentLiteral") {
305                return;
306            }
307            for member in members.values_mut() {
308                strip_nonsemantic_attributes(member);
309            }
310            if let Some(serde_json::Value::Object(attributes)) = members.get_mut("attributes") {
311                attributes.remove("source");
312                attributes.remove("extensions");
313                if attributes.is_empty() {
314                    members.remove("attributes");
315                }
316            }
317        }
318        _ => {}
319    }
320}
321
322fn write_canonical_json(
323    value: &serde_json::Value,
324    output: &mut Vec<u8>,
325) -> Result<(), serde_json::Error> {
326    use serde_json::Value;
327    match value {
328        Value::Null => output.extend_from_slice(b"null"),
329        Value::Bool(flag) => output.extend_from_slice(if *flag { b"true" } else { b"false" }),
330        Value::Number(number) => output.extend_from_slice(number.to_string().as_bytes()),
331        Value::String(text) => output.extend_from_slice(serde_json::to_string(text)?.as_bytes()),
332        Value::Array(items) => {
333            output.push(b'[');
334            for (index, item) in items.iter().enumerate() {
335                if index > 0 {
336                    output.push(b',');
337                }
338                write_canonical_json(item, output)?;
339            }
340            output.push(b']');
341        }
342        Value::Object(members) => {
343            output.push(b'{');
344            let mut keys = members.keys().collect::<Vec<_>>();
345            keys.sort_unstable_by(|left, right| left.as_bytes().cmp(right.as_bytes()));
346            for (index, key) in keys.into_iter().enumerate() {
347                if index > 0 {
348                    output.push(b',');
349                }
350                output.extend_from_slice(serde_json::to_string(key)?.as_bytes());
351                output.push(b':');
352                write_canonical_json(&members[key], output)?;
353            }
354            output.push(b'}');
355        }
356    }
357    Ok(())
358}
359
360/// A portable address with a canonical Morphir URI spelling.
361#[derive(Debug, Clone, PartialEq, Eq)]
362pub struct NodeUri {
363    artifact: ArtifactSelector,
364    format: IrFormatVersion,
365    root: NodeRoot,
366    steps: Vec<NodeStep>,
367    revision: ArtifactRevision,
368    guard: Option<Sha256Digest>,
369}
370
371impl NodeUri {
372    /// Construct an address only if its canonical spelling parses back to the
373    /// same typed value. This also checks the guard and name invariants.
374    pub fn new(
375        artifact: ArtifactSelector,
376        format: IrFormatVersion,
377        root: NodeRoot,
378        steps: Vec<NodeStep>,
379        revision: ArtifactRevision,
380        guard: Option<Sha256Digest>,
381    ) -> Result<Self, NodeUriError> {
382        let candidate = Self {
383            artifact,
384            format,
385            root,
386            steps,
387            revision,
388            guard,
389        };
390        let checked = Self::parse(&candidate.to_string())?;
391        if checked != candidate {
392            return Err(invalid("address is not canonical"));
393        }
394        Ok(candidate)
395    }
396
397    pub fn artifact(&self) -> &ArtifactSelector {
398        &self.artifact
399    }
400    pub fn format(&self) -> IrFormatVersion {
401        self.format
402    }
403    pub fn root(&self) -> &NodeRoot {
404        &self.root
405    }
406    pub fn steps(&self) -> &[NodeStep] {
407        &self.steps
408    }
409    pub fn revision(&self) -> &ArtifactRevision {
410        &self.revision
411    }
412    pub fn guard(&self) -> Option<&Sha256Digest> {
413        self.guard.as_ref()
414    }
415
416    /// Parse one canonical draft node URI, rejecting aliases and unknown roles.
417    pub fn parse(uri: &str) -> Result<Self, NodeUriError> {
418        let body = uri
419            .strip_prefix("morphir://ir/")
420            .ok_or_else(|| invalid("wrong URI authority"))?;
421        let (before_fragment, fragment) = body
422            .split_once('#')
423            .ok_or_else(|| invalid("missing node fragment"))?;
424        if fragment.contains('#') {
425            return Err(invalid("duplicate fragment"));
426        }
427        let (selector, query) = before_fragment
428            .split_once('?')
429            .ok_or_else(|| invalid("missing format query"))?;
430        let artifact = if let Some(raw) = selector.strip_prefix("pkg/") {
431            if raw.is_empty() {
432                return Err(invalid("empty package selector"));
433            }
434            ArtifactSelector::Package(parse_package(raw)?)
435        } else if let Some(alias) = selector.strip_prefix("workspace/") {
436            if !valid_workspace_alias(alias) {
437                return Err(invalid("invalid workspace alias"));
438            }
439            ArtifactSelector::Workspace(alias.to_owned())
440        } else {
441            return Err(invalid("unknown artifact selector"));
442        };
443
444        let mut format = None;
445        let mut revision = ArtifactRevision::Current;
446        let mut guard = None;
447        for parameter in query.split('&') {
448            let (key, value) = parameter
449                .split_once('=')
450                .ok_or_else(|| invalid("malformed query parameter"))?;
451            match key {
452                "format" if format.is_none() => format = Some(parse_format(value)?),
453                "rev" if matches!(revision, ArtifactRevision::Current) => {
454                    revision = ArtifactRevision::Pinned(Sha256Digest::parse(value)?)
455                }
456                "guard" if guard.is_none() => guard = Some(Sha256Digest::parse(value)?),
457                _ => return Err(invalid("duplicate or unknown query parameter")),
458            }
459        }
460        let format = format.ok_or_else(|| invalid("missing format"))?;
461        let parts = fragment
462            .strip_prefix('/')
463            .ok_or_else(|| invalid("node fragment must start with /"))?
464            .split('/')
465            .collect::<Vec<_>>();
466        let (root, consumed) = parse_root(&parts)?;
467        let steps = parse_steps(&parts[consumed..])?;
468        if matches!(revision, ArtifactRevision::Current)
469            && guard.is_none()
470            && steps.iter().any(NodeStep::is_positional)
471        {
472            return Err(NodeUriError::MissingGuard);
473        }
474        let address = Self {
475            artifact,
476            format,
477            root,
478            steps,
479            revision,
480            guard,
481        };
482        if address.to_string() != uri {
483            return Err(invalid("noncanonical URI spelling"));
484        }
485        Ok(address)
486    }
487}
488
489impl fmt::Display for NodeUri {
490    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
491        write!(f, "morphir://ir/")?;
492        match &self.artifact {
493            ArtifactSelector::Package(package) => {
494                write!(f, "pkg/{}", package.to_canonical_string())?
495            }
496            ArtifactSelector::Workspace(alias) => write!(f, "workspace/{alias}")?,
497        }
498        write!(f, "?format={}", self.format)?;
499        if let ArtifactRevision::Pinned(digest) = &self.revision {
500            write!(f, "&rev={digest}")?;
501        }
502        if let Some(digest) = &self.guard {
503            write!(f, "&guard={digest}")?;
504        }
505        write!(f, "#")?;
506        let mut root = Vec::new();
507        match &self.root {
508            NodeRoot::Distribution => root.push("distribution".to_owned()),
509            NodeRoot::Package => root.push("package".to_owned()),
510            NodeRoot::Dependency(package) => {
511                root.extend([
512                    "dependency".to_owned(),
513                    encode_component(&package.to_canonical_string()),
514                ]);
515            }
516            NodeRoot::EntryPoint(name) => {
517                root.extend(["entry-point".to_owned(), encode_component(name)])
518            }
519            NodeRoot::Module { owner, module } => {
520                push_owner(&mut root, owner);
521                root.extend([
522                    "module".to_owned(),
523                    encode_component(&module.to_canonical_string()),
524                ]);
525            }
526            NodeRoot::Type {
527                owner,
528                module,
529                name,
530            }
531            | NodeRoot::Value {
532                owner,
533                module,
534                name,
535            } => {
536                push_owner(&mut root, owner);
537                root.extend([
538                    "module".to_owned(),
539                    encode_component(&module.to_canonical_string()),
540                    if matches!(self.root, NodeRoot::Type { .. }) {
541                        "type"
542                    } else {
543                        "value"
544                    }
545                    .to_owned(),
546                    encode_component(&name.to_canonical_string()),
547                ]);
548            }
549        }
550        for step in &self.steps {
551            match step {
552                NodeStep::TypeExpression => root.push("type-exp".to_owned()),
553                NodeStep::Body => root.push("body".to_owned()),
554                NodeStep::ValueInputType(name) => root.extend([
555                    "input".to_owned(),
556                    encode_component(&name.to_canonical_string()),
557                ]),
558                NodeStep::ValueInputAnnotation(name) => root.extend([
559                    "input-annotation".to_owned(),
560                    encode_component(&name.to_canonical_string()),
561                ]),
562                NodeStep::ValueOutputType => root.push("output".to_owned()),
563                NodeStep::DerivedBaseType => {
564                    root.extend(["derived".to_owned(), "base-type".to_owned()])
565                }
566                NodeStep::PartialTypeExpression => {
567                    root.extend(["incomplete".to_owned(), "partial-type".to_owned()])
568                }
569                NodeStep::CustomConstructor(name) => root.extend([
570                    "constructor".to_owned(),
571                    encode_component(&name.to_canonical_string()),
572                ]),
573                NodeStep::ConstructorArgument(index) => {
574                    root.extend(["argument".to_owned(), index.to_string()])
575                }
576                NodeStep::RecordField(name) => root.extend([
577                    "record".to_owned(),
578                    "field".to_owned(),
579                    encode_component(&name.to_canonical_string()),
580                ]),
581                NodeStep::ExtensibleRecordField(name) => root.extend([
582                    "extensible-record".to_owned(),
583                    "field".to_owned(),
584                    encode_component(&name.to_canonical_string()),
585                ]),
586                NodeStep::TypeFunctionParameter => {
587                    root.extend(["function".to_owned(), "parameter".to_owned()])
588                }
589                NodeStep::TypeFunctionResult => {
590                    root.extend(["function".to_owned(), "result".to_owned()])
591                }
592                NodeStep::ReferenceArgument(index) => root.extend([
593                    "reference".to_owned(),
594                    "argument".to_owned(),
595                    index.to_string(),
596                ]),
597                NodeStep::ApplyFunction => root.extend(["apply".to_owned(), "function".to_owned()]),
598                NodeStep::ApplyArgument => root.extend(["apply".to_owned(), "argument".to_owned()]),
599                NodeStep::FieldSubject => root.extend(["field".to_owned(), "subject".to_owned()]),
600                NodeStep::DestructurePattern => {
601                    root.extend(["destructure".to_owned(), "pattern".to_owned()])
602                }
603                NodeStep::DestructureValue => {
604                    root.extend(["destructure".to_owned(), "value".to_owned()])
605                }
606                NodeStep::DestructureBody => {
607                    root.extend(["destructure".to_owned(), "body".to_owned()])
608                }
609                NodeStep::IfCondition => root.extend(["if".to_owned(), "condition".to_owned()]),
610                NodeStep::IfThen => root.extend(["if".to_owned(), "then".to_owned()]),
611                NodeStep::IfElse => root.extend(["if".to_owned(), "else".to_owned()]),
612                NodeStep::LambdaPattern => root.extend(["lambda".to_owned(), "pattern".to_owned()]),
613                NodeStep::LambdaBody => root.extend(["lambda".to_owned(), "body".to_owned()]),
614                NodeStep::LetDefinition(name) => root.extend([
615                    "let".to_owned(),
616                    "definition".to_owned(),
617                    encode_component(&name.to_canonical_string()),
618                ]),
619                NodeStep::LetBody => root.extend(["let".to_owned(), "body".to_owned()]),
620                NodeStep::ListElement(index) => {
621                    root.extend(["list".to_owned(), "element".to_owned(), index.to_string()])
622                }
623                NodeStep::PatternMatchSubject => {
624                    root.extend(["pattern-match".to_owned(), "subject".to_owned()])
625                }
626                NodeStep::TupleElement(index) => {
627                    root.extend(["tuple".to_owned(), "element".to_owned(), index.to_string()])
628                }
629                NodeStep::PatternMatchCasePattern(index) => root.extend([
630                    "pattern-match".to_owned(),
631                    "case".to_owned(),
632                    index.to_string(),
633                    "pattern".to_owned(),
634                ]),
635                NodeStep::PatternMatchCaseBody(index) => root.extend([
636                    "pattern-match".to_owned(),
637                    "case".to_owned(),
638                    index.to_string(),
639                    "body".to_owned(),
640                ]),
641                NodeStep::UpdateSubject => root.extend(["update".to_owned(), "subject".to_owned()]),
642                NodeStep::UpdateField(name) => root.extend([
643                    "update".to_owned(),
644                    "field".to_owned(),
645                    encode_component(&name.to_canonical_string()),
646                ]),
647                NodeStep::AsPatternChild => {
648                    root.extend(["as-pattern".to_owned(), "pattern".to_owned()])
649                }
650                NodeStep::PatternTupleElement(index) => root.extend([
651                    "tuple-pattern".to_owned(),
652                    "element".to_owned(),
653                    index.to_string(),
654                ]),
655                NodeStep::PatternConstructorArgument(index) => root.extend([
656                    "constructor-pattern".to_owned(),
657                    "argument".to_owned(),
658                    index.to_string(),
659                ]),
660                NodeStep::HeadTailHead => root.extend(["head-tail".to_owned(), "head".to_owned()]),
661                NodeStep::HeadTailTail => root.extend(["head-tail".to_owned(), "tail".to_owned()]),
662                NodeStep::ExternalFallback => {
663                    root.extend(["external".to_owned(), "fallback".to_owned()])
664                }
665                NodeStep::IncompletePartialBody => {
666                    root.extend(["incomplete".to_owned(), "partial-body".to_owned()])
667                }
668                NodeStep::HoleExpectedType => {
669                    root.extend(["hole".to_owned(), "expected-type".to_owned()])
670                }
671                NodeStep::InferredType => root.push("inferred-type".to_owned()),
672                NodeStep::AnnotationEntry(index) => root.extend([
673                    "annotation".to_owned(),
674                    "entry".to_owned(),
675                    index.to_string(),
676                ]),
677                NodeStep::AnnotationArgument(index) => {
678                    root.extend(["argument".to_owned(), index.to_string()])
679                }
680            }
681        }
682        write!(f, "/{}", root.join("/"))
683    }
684}
685
686fn invalid(message: &str) -> NodeUriError {
687    NodeUriError::Invalid(message.to_owned())
688}
689
690fn parse_format(value: &str) -> Result<IrFormatVersion, NodeUriError> {
691    let parts = value.split('.').collect::<Vec<_>>();
692    if parts.len() != 3
693        || parts
694            .iter()
695            .any(|part| part.is_empty() || (part.len() > 1 && part.starts_with('0')))
696    {
697        return Err(invalid(
698            "format must have three canonical numeric components",
699        ));
700    }
701    let parse = |part: &str| {
702        part.parse::<u32>()
703            .map_err(|_| invalid("invalid format component"))
704    };
705    Ok(IrFormatVersion::new(
706        parse(parts[0])?,
707        parse(parts[1])?,
708        parse(parts[2])?,
709    ))
710}
711
712fn parse_package(value: &str) -> Result<PackageName, NodeUriError> {
713    let package =
714        PackageName::from_canonical_string(value).map_err(|_| invalid("invalid package name"))?;
715    if package.is_empty() || package.to_canonical_string() != value {
716        return Err(invalid("noncanonical package name"));
717    }
718    Ok(package)
719}
720
721fn parse_path(value: &str) -> Result<Path, NodeUriError> {
722    let decoded = decode_component(value)?;
723    let path = Path::from_canonical_string(&decoded).map_err(|_| invalid("invalid module path"))?;
724    if path.is_empty() || path.to_canonical_string() != decoded {
725        return Err(invalid("noncanonical module path"));
726    }
727    Ok(path)
728}
729
730fn parse_name(value: &str) -> Result<Name, NodeUriError> {
731    let decoded = decode_component(value)?;
732    let name = Name::from_canonical_string(&decoded).map_err(|_| invalid("invalid name"))?;
733    if name.to_canonical_string() != decoded {
734        return Err(invalid("noncanonical name"));
735    }
736    Ok(name)
737}
738
739fn parse_root(parts: &[&str]) -> Result<(NodeRoot, usize), NodeUriError> {
740    match parts {
741        ["distribution", ..] => Ok((NodeRoot::Distribution, 1)),
742        ["package", ..] => Ok((NodeRoot::Package, 1)),
743        ["entry-point", raw, ..] if !raw.is_empty() => {
744            Ok((NodeRoot::EntryPoint(decode_component(raw)?), 2))
745        }
746        _ => {
747            let (owner, rest, prefix) = match parts {
748                ["dependency", raw, rest @ ..] => (
749                    NodeOwner::Dependency(parse_package(&decode_component(raw)?)?),
750                    rest,
751                    2,
752                ),
753                other => (NodeOwner::OwnPackage, other, 0),
754            };
755            if rest.is_empty()
756                && prefix == 2
757                && let NodeOwner::Dependency(package) = owner
758            {
759                return Ok((NodeRoot::Dependency(package), 2));
760            }
761            match rest {
762                ["module", module, "type", name, ..] => Ok((
763                    NodeRoot::Type {
764                        owner,
765                        module: parse_path(module)?,
766                        name: parse_name(name)?,
767                    },
768                    prefix + 4,
769                )),
770                ["module", module, "value", name, ..] => Ok((
771                    NodeRoot::Value {
772                        owner,
773                        module: parse_path(module)?,
774                        name: parse_name(name)?,
775                    },
776                    prefix + 4,
777                )),
778                ["module", module, ..] => Ok((
779                    NodeRoot::Module {
780                        owner,
781                        module: parse_path(module)?,
782                    },
783                    prefix + 2,
784                )),
785                _ => Err(invalid("invalid node root")),
786            }
787        }
788    }
789}
790
791fn parse_steps(parts: &[&str]) -> Result<Vec<NodeStep>, NodeUriError> {
792    let mut steps = Vec::new();
793    let mut cursor = parts;
794    while !cursor.is_empty() {
795        let (step, count) = match cursor {
796            ["type-exp", ..] => (NodeStep::TypeExpression, 1),
797            ["body", ..] => (NodeStep::Body, 1),
798            ["input", name, ..] => (NodeStep::ValueInputType(parse_name(name)?), 2),
799            ["input-annotation", name, ..] => {
800                (NodeStep::ValueInputAnnotation(parse_name(name)?), 2)
801            }
802            ["output", ..] => (NodeStep::ValueOutputType, 1),
803            ["derived", "base-type", ..] => (NodeStep::DerivedBaseType, 2),
804            ["incomplete", "partial-type", ..] => (NodeStep::PartialTypeExpression, 2),
805            ["constructor", name, ..] => (NodeStep::CustomConstructor(parse_name(name)?), 2),
806            ["argument", index, ..] => (
807                if matches!(steps.last(), Some(NodeStep::AnnotationEntry(_))) {
808                    NodeStep::AnnotationArgument(parse_index(index)?)
809                } else {
810                    NodeStep::ConstructorArgument(parse_index(index)?)
811                },
812                2,
813            ),
814            ["record", "field", name, ..] => (NodeStep::RecordField(parse_name(name)?), 3),
815            ["extensible-record", "field", name, ..] => {
816                (NodeStep::ExtensibleRecordField(parse_name(name)?), 3)
817            }
818            ["function", "parameter", ..] => (NodeStep::TypeFunctionParameter, 2),
819            ["function", "result", ..] => (NodeStep::TypeFunctionResult, 2),
820            ["reference", "argument", index, ..] => {
821                (NodeStep::ReferenceArgument(parse_index(index)?), 3)
822            }
823            ["apply", "function", ..] => (NodeStep::ApplyFunction, 2),
824            ["apply", "argument", ..] => (NodeStep::ApplyArgument, 2),
825            ["field", "subject", ..] => (NodeStep::FieldSubject, 2),
826            ["destructure", "pattern", ..] => (NodeStep::DestructurePattern, 2),
827            ["destructure", "value", ..] => (NodeStep::DestructureValue, 2),
828            ["destructure", "body", ..] => (NodeStep::DestructureBody, 2),
829            ["if", "condition", ..] => (NodeStep::IfCondition, 2),
830            ["if", "then", ..] => (NodeStep::IfThen, 2),
831            ["if", "else", ..] => (NodeStep::IfElse, 2),
832            ["lambda", "pattern", ..] => (NodeStep::LambdaPattern, 2),
833            ["lambda", "body", ..] => (NodeStep::LambdaBody, 2),
834            ["let", "definition", name, ..] => (NodeStep::LetDefinition(parse_name(name)?), 3),
835            ["let", "body", ..] => (NodeStep::LetBody, 2),
836            ["list", "element", index, ..] => (NodeStep::ListElement(parse_index(index)?), 3),
837            ["pattern-match", "subject", ..] => (NodeStep::PatternMatchSubject, 2),
838            ["tuple", "element", index, ..] => (NodeStep::TupleElement(parse_index(index)?), 3),
839            ["pattern-match", "case", index, "pattern", ..] => {
840                (NodeStep::PatternMatchCasePattern(parse_index(index)?), 4)
841            }
842            ["pattern-match", "case", index, "body", ..] => {
843                (NodeStep::PatternMatchCaseBody(parse_index(index)?), 4)
844            }
845            ["update", "subject", ..] => (NodeStep::UpdateSubject, 2),
846            ["update", "field", name, ..] => (NodeStep::UpdateField(parse_name(name)?), 3),
847            ["as-pattern", "pattern", ..] => (NodeStep::AsPatternChild, 2),
848            ["tuple-pattern", "element", index, ..] => {
849                (NodeStep::PatternTupleElement(parse_index(index)?), 3)
850            }
851            ["constructor-pattern", "argument", index, ..] => {
852                (NodeStep::PatternConstructorArgument(parse_index(index)?), 3)
853            }
854            ["head-tail", "head", ..] => (NodeStep::HeadTailHead, 2),
855            ["head-tail", "tail", ..] => (NodeStep::HeadTailTail, 2),
856            ["external", "fallback", ..] => (NodeStep::ExternalFallback, 2),
857            ["incomplete", "partial-body", ..] => (NodeStep::IncompletePartialBody, 2),
858            ["hole", "expected-type", ..] => (NodeStep::HoleExpectedType, 2),
859            ["inferred-type", ..] => (NodeStep::InferredType, 1),
860            ["annotation", "entry", index, ..] => {
861                (NodeStep::AnnotationEntry(parse_index(index)?), 3)
862            }
863            _ => return Err(invalid("unknown semantic child role")),
864        };
865        steps.push(step);
866        cursor = &cursor[count..];
867    }
868    Ok(steps)
869}
870
871fn parse_index(value: &str) -> Result<usize, NodeUriError> {
872    if value.is_empty()
873        || (value.len() > 1 && value.starts_with('0'))
874        || !value.bytes().all(|byte| byte.is_ascii_digit())
875    {
876        return Err(invalid("invalid positional index"));
877    }
878    value
879        .parse()
880        .map_err(|_| invalid("positional index overflow"))
881}
882
883fn valid_workspace_alias(value: &str) -> bool {
884    !value.is_empty()
885        && value.split('-').all(|part| {
886            !part.is_empty()
887                && part
888                    .bytes()
889                    .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit())
890        })
891        && value.as_bytes()[0].is_ascii_lowercase()
892}
893
894fn push_owner(parts: &mut Vec<String>, owner: &NodeOwner) {
895    if let NodeOwner::Dependency(package) = owner {
896        parts.extend([
897            "dependency".to_owned(),
898            encode_component(&package.to_canonical_string()),
899        ]);
900    }
901}
902
903fn encode_component(value: &str) -> String {
904    let mut result = String::new();
905    for byte in value.bytes() {
906        if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~') {
907            result.push(byte as char);
908        } else {
909            result.push_str(&format!("%{byte:02X}"));
910        }
911    }
912    result
913}
914
915fn decode_component(value: &str) -> Result<String, NodeUriError> {
916    let mut bytes = Vec::new();
917    let mut input = value.bytes();
918    while let Some(byte) = input.next() {
919        if byte == b'%' {
920            let high = input
921                .next()
922                .ok_or_else(|| invalid("truncated percent escape"))?;
923            let low = input
924                .next()
925                .ok_or_else(|| invalid("truncated percent escape"))?;
926            let hex = |digit: u8| match digit {
927                b'0'..=b'9' => Some(digit - b'0'),
928                b'A'..=b'F' => Some(digit - b'A' + 10),
929                _ => None,
930            };
931            bytes.push(
932                hex(high)
933                    .zip(hex(low))
934                    .map(|(high, low)| high * 16 + low)
935                    .ok_or_else(|| invalid("invalid percent escape"))?,
936            );
937        } else {
938            if !byte.is_ascii() {
939                return Err(invalid("raw non-ASCII URI component"));
940            }
941            bytes.push(byte);
942        }
943    }
944    String::from_utf8(bytes).map_err(|_| invalid("invalid UTF-8 escape"))
945}