Skip to main content

qualia_core_db/query/
resolver.rs

1//! Zero-allocation Lexicon Resolver.
2//!
3//! Maps 64-bit Quin field values back to human-readable `&[u8]` slices for
4//! serialisation into N-Triples or JSON-LD surface syntaxes.
5//!
6//! # Bit layout of a Quin field value
7//!
8//! ```text
9//! Bit 63  │ Bits 60-63   │ Bits 0-59     │ Interpretation
10//! ────────┼──────────────┼───────────────┼──────────────────────────────────────
11//!   1     │ any          │ payload       │ did:q42 topological pointer (identifier module)
12//!   0     │ 0b0000       │ FNV-1a hash   │ IRI / blank-node → lexicon lookup
13//!   0     │ 0b0001       │ integer       │ Inline xsd:integer literal
14//!   0     │ 0b0010       │ scaled × 10⁶  │ Inline xsd:decimal literal
15//!   0     │ 0b0011       │ 0 or 1        │ Inline xsd:boolean literal
16//!   0     │ 0b001        │ embedded hash │ SPARQL-Star embedded triple <<s p o>>
17//!   0     │ 0b1000       │ webizen id    │ Person-controlled WebID agent identifier
18//!   0     │ 0b0101       │ f32 bits      │ Inline xsd:float literal (computed values)
19//!   0     │ 0b0110–0b0111│ reserved      │ Treated as IRI hash (future use)
20//! ```
21//!
22//! NOTE: `0b0101` (`INLINE_TAG_FLOAT`) was formally allocated to inline `xsd:float`
23//! in 0.0.19 to resolve the float-vs-integer tag clash — computed f32 values used to
24//! squat on the `0b0001` integer tag. The Webizen VM, `frame_layout`, and this
25//! resolver now agree on `0b0101`. See `AGENTS.md §4-D` and `ALGEBRA_MANIFOLD_PLAN.md`.
26//!
27//! The inline-type encoding is applied by the ingest layer, which masks
28//! FNV-1a hash values to 60 bits before storing them so there is no
29//! ambiguity with the type-tag bits.
30//!
31//! # Zero-allocation guarantee
32//! `format_ntriples_to` writes directly to any `impl io::Write` sink and
33//! never touches the heap.  Callers own the output buffer.
34
35use crate::NQuin;
36use std::io;
37
38// ---------------------------------------------------------------------------
39// Bit-layout constants
40// ---------------------------------------------------------------------------
41
42pub const MSB_FLAG: u64 = 1u64 << 63;
43pub const INLINE_TAG_MASK: u64 = 0b111u64 << 60; // bits 60-62 (only when MSB=0)
44pub const INLINE_TAG_INTEGER: u64 = 0b001u64 << 60;
45pub const INLINE_TAG_DECIMAL: u64 = 0b010u64 << 60;
46pub const INLINE_TAG_BOOLEAN: u64 = 0b011u64 << 60;
47/// Inline `xsd:float` literal: bits 0-31 are raw IEEE-754 f32 bits. Allocated 0.0.19 to
48/// end the float-vs-integer clash (formerly squatted on `INLINE_TAG_INTEGER`). Canonical
49/// home for this tag; `frame_layout` re-exports it.
50pub const INLINE_TAG_FLOAT: u64 = 0b101u64 << 60;
51/// SPARQL-Star embedded triple tag: indicates the value is a Virtual ID for <<s p o>>
52pub const TAG_EMBEDDED: u64 = 0b001u64 << 60;
53/// Webizen identity tag: indicates the value is a person-controlled WebID agent identifier
54/// Uses 0x8 prefix for instant identification without dictionary lookup
55pub const TAG_WEBIZEN: u64 = 0b1000u64 << 60;
56/// Mask over bits 0-59 — the value payload when an inline tag is present.
57pub const INLINE_VALUE_MASK: u64 = !(MSB_FLAG | INLINE_TAG_MASK);
58
59// ---------------------------------------------------------------------------
60// Demo lexicon
61// ---------------------------------------------------------------------------
62// In production this static table is replaced by a memory-mapped `.q42`
63// dictionary shard with entries sorted by hash for O(log n) binary search.
64// For the current phase a linear scan over this small table is sufficient.
65//
66// Entries are (fnv1a_hash, iri_bytes).  Hashes are computed at compile time
67// via `q_hash`, which is `const fn`.
68
69static DEMO_LEXICON: &[(u64, &[u8])] = &[
70    (crate::q_hash("Alice"), b"http://webizen.org/demo/Alice"),
71    (crate::q_hash("Bob"), b"http://webizen.org/demo/Bob"),
72    (crate::q_hash("Carol"), b"http://webizen.org/demo/Carol"),
73    (crate::q_hash("knows"), b"http://schema.org/knows"),
74    (crate::q_hash("likes"), b"http://schema.org/likes"),
75    (
76        crate::q_hash("type"),
77        b"http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
78    ),
79    (
80        crate::q_hash("label"),
81        b"http://www.w3.org/2000/01/rdf-schema#label",
82    ),
83    (crate::q_hash("Person"), b"http://schema.org/Person"),
84    (crate::q_hash("name"), b"http://schema.org/name"),
85    (
86        crate::q_hash("guardian"),
87        b"http://webizen.org/vocab#guardian",
88    ),
89    (crate::q_hash("ward"), b"http://webizen.org/vocab#ward"),
90    (
91        crate::q_hash("has_symptom"),
92        b"http://webizen.org/medical#hasSymptom",
93    ),
94    (crate::q_hash("Fever"), b"http://snomed.info/id/386661006"),
95    (
96        crate::q_hash("income"),
97        b"http://webizen.org/finance#income",
98    ),
99    (
100        crate::q_hash("balance"),
101        b"http://webizen.org/finance#balance",
102    ),
103];
104
105// ---------------------------------------------------------------------------
106// Lexicon struct
107// ---------------------------------------------------------------------------
108
109/// Wraps the persistent dictionary block used for hash → IRI resolution.
110///
111/// In production this struct holds a raw pointer into a memory-mapped `.q42`
112/// dictionary shard and resolves lookups via a binary search over the
113/// sorted `(hash, byte_offset)` index — zero copies, zero allocations.
114///
115/// For the current phase it wraps the compile-time `DEMO_LEXICON` table.
116pub struct Lexicon {
117    entries: &'static [(u64, &'static [u8])],
118}
119
120impl Lexicon {
121    pub const fn new() -> Self {
122        Self {
123            entries: DEMO_LEXICON,
124        }
125    }
126
127    /// Look up `hash` in the dictionary.
128    ///
129    /// Production upgrade: sort `entries` by hash and replace the linear scan
130    /// with `entries.binary_search_by_key(&hash, |&(h, _)| h)`.
131    #[inline]
132    pub fn resolve(&self, hash: u64) -> Option<&'static [u8]> {
133        for &(h, bytes) in self.entries {
134            if h == hash {
135                return Some(bytes);
136            }
137        }
138        None
139    }
140}
141
142const LEXICON: Lexicon = Lexicon::new();
143
144// ---------------------------------------------------------------------------
145// Public resolution API
146// ---------------------------------------------------------------------------
147
148/// Resolve a 64-bit Quin field value to its original URI bytes.
149///
150/// Returns `None` when:
151/// - the value has MSB=1 (topological pointer — not a lexicon entry), or
152/// - the hash is genuinely absent from the dictionary.
153pub fn resolve_hash(hash: u64) -> Option<&'static [u8]> {
154    // Topological pointers (MSB=1 and NOT in the lexicon) are not dictionary
155    // entries.  Check the lexicon first so that hashes whose FNV-1a value
156    // naturally has bit 63 set are still resolved correctly.
157    if let Some(uri) = LEXICON.resolve(hash) {
158        return Some(uri);
159    }
160    if (hash & MSB_FLAG) != 0 {
161        return None; // confirmed topological pointer
162    }
163    None
164}
165
166// ---------------------------------------------------------------------------
167// Term formatters  (all write to impl io::Write — no heap allocation)
168// ---------------------------------------------------------------------------
169
170/// Write a subject or predicate term.
171/// These positions hold only IRI hashes or did:q42 topological pointers —
172/// no inline-typed literals.
173///
174/// **Lexicon takes priority over bit-flag detection.**
175/// A hash stored in the dictionary is always rendered as an IRI, regardless of
176/// which bits happen to be set by FNV-1a.  Only values that are absent from the
177/// lexicon AND have MSB=1 are interpreted as `did:q42` topological pointers.
178/// This correctly handles terms like `q_hash("knows")` whose FNV-1a output
179/// naturally has bit 63 set.
180#[inline]
181pub(crate) fn write_iri_term<W: io::Write>(val: u64, out: &mut W) -> io::Result<()> {
182    // 1. Lexicon lookup — exact value, no bit-stripping.
183    if let Some(uri) = LEXICON.resolve(val) {
184        out.write_all(b"<")?;
185        out.write_all(uri)?;
186        return out.write_all(b">");
187    }
188    // 2. Not in lexicon + MSB set → did:q42 topological pointer.
189    if (val & MSB_FLAG) != 0 {
190        let ptr = val & !MSB_FLAG;
191        return write!(out, "<did:q42:ptr/{ptr:016x}>");
192    }
193    // 3. Unknown hash — hex fallback.
194    write!(out, "<quin:hash/{val:016x}>")
195}
196
197/// An inline-typed literal decoded from a Quin field value's tag bits (60-62).
198/// Allocation-free; the caller formats the lexical/datatype form it needs
199/// (N-Triples surface syntax, SPARQL-Results JSON/XML, etc.).
200#[derive(Debug, Clone, Copy, PartialEq)]
201pub enum InlineLiteral {
202    /// `xsd:integer` (already 60-bit sign-extended to i64).
203    Integer(i64),
204    /// `xsd:decimal` — fixed-point, the value is `raw × 10⁻⁶` (raw sign-extended).
205    Decimal(i64),
206    /// `xsd:boolean`.
207    Boolean(bool),
208    /// `xsd:float` (decoded from the lower 32 bits as IEEE-754 f32).
209    Float(f32),
210}
211
212impl InlineLiteral {
213    /// The XSD datatype IRI (without angle brackets) for this literal.
214    pub fn datatype_iri(&self) -> &'static str {
215        match self {
216            InlineLiteral::Integer(_) => "http://www.w3.org/2001/XMLSchema#integer",
217            InlineLiteral::Decimal(_) => "http://www.w3.org/2001/XMLSchema#decimal",
218            InlineLiteral::Boolean(_) => "http://www.w3.org/2001/XMLSchema#boolean",
219            InlineLiteral::Float(_) => "http://www.w3.org/2001/XMLSchema#float",
220        }
221    }
222}
223
224impl std::fmt::Display for InlineLiteral {
225    /// The canonical lexical form (the string that goes between the quotes).
226    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
227        match *self {
228            InlineLiteral::Integer(n) => write!(f, "{n}"),
229            InlineLiteral::Decimal(raw) => {
230                let neg = raw < 0;
231                let abs = raw.unsigned_abs();
232                let whole = abs / 1_000_000;
233                let frac = abs % 1_000_000;
234                if neg {
235                    write!(f, "-{whole}.{frac:06}")
236                } else {
237                    write!(f, "{whole}.{frac:06}")
238                }
239            }
240            InlineLiteral::Boolean(b) => write!(f, "{b}"),
241            InlineLiteral::Float(x) => write!(f, "{x}"),
242        }
243    }
244}
245
246/// Classify a Quin field value as an inline-typed literal, or `None` if it is
247/// not one (an IRI hash / did:q42 pointer / lexicon entry).
248///
249/// IMPORTANT: `INLINE_TAG_INTEGER` and `TAG_EMBEDDED` are the *same* bit pattern
250/// (`0b001 << 60`). A caller that also handles SPARQL-Star embedded triples must
251/// try its embedded-triple lexicon lookup **before** calling this — a resolvable
252/// embedded-triple virtual id would otherwise be reported here as an integer.
253#[inline]
254pub fn classify_inline_literal(val: u64) -> Option<InlineLiteral> {
255    // A value with the MSB set is a topological pointer, never an inline literal.
256    if (val & MSB_FLAG) != 0 {
257        return None;
258    }
259    match val & INLINE_TAG_MASK {
260        INLINE_TAG_INTEGER => {
261            let mut n = (val & INLINE_VALUE_MASK) as i64;
262            if (n & (1i64 << 59)) != 0 {
263                n |= !((1i64 << 60) - 1);
264            }
265            Some(InlineLiteral::Integer(n))
266        }
267        INLINE_TAG_DECIMAL => {
268            let mut raw = (val & INLINE_VALUE_MASK) as i64;
269            if (raw & (1i64 << 59)) != 0 {
270                raw |= !((1i64 << 60) - 1);
271            }
272            Some(InlineLiteral::Decimal(raw))
273        }
274        INLINE_TAG_BOOLEAN => Some(InlineLiteral::Boolean((val & 1) != 0)),
275        INLINE_TAG_FLOAT => Some(InlineLiteral::Float(f32::from_bits(
276            (val & 0xFFFF_FFFF) as u32,
277        ))),
278        _ => None,
279    }
280}
281
282/// Write an object term, applying inline-type detection on bits 60-62.
283///
284/// Priority order (same lexicon-first reasoning as `write_iri_term`):
285/// 1. Lexicon match → IRI
286/// 2. MSB=1 and not in lexicon → did:q42 pointer
287/// 3. Bits 60-62 match a known inline tag → typed literal
288/// 4. Fallback → hex placeholder
289#[inline]
290pub(crate) fn write_object_term<W: io::Write>(val: u64, out: &mut W) -> io::Result<()> {
291    // 1. Lexicon first — a known IRI hash wins over any bit-pattern check.
292    if let Some(uri) = LEXICON.resolve(val) {
293        out.write_all(b"<")?;
294        out.write_all(uri)?;
295        return out.write_all(b">");
296    }
297    // 2. MSB=1 → topological pointer.
298    if (val & MSB_FLAG) != 0 {
299        let ptr = val & !MSB_FLAG;
300        return write!(out, "<did:q42:ptr/{ptr:016x}>");
301    }
302    // 3. Inline-type detection (only reached for values NOT in the lexicon).
303    //    The ingest layer encodes typed literals with explicit tag bits, so
304    //    there is no ambiguity with normalised IRI hashes in a live database.
305    //    Shares one decoder with `classify_inline_literal` so the (error-prone)
306    //    sign-extension / f32 decode lives in exactly one place.
307    match classify_inline_literal(val) {
308        Some(lit) => write!(out, "\"{lit}\"^^<{}>", lit.datatype_iri()),
309        None => write!(out, "<quin:hash/{val:016x}>"),
310    }
311}
312
313// ---------------------------------------------------------------------------
314// Public streaming formatter
315// ---------------------------------------------------------------------------
316
317/// Serialise `quins` as N-Triples, writing each line directly to `out`.
318///
319/// The function itself performs **no heap allocation** — it writes bytes
320/// directly to the caller-supplied `W` sink.  Callers that need an in-memory
321/// buffer should pass `&mut Vec<u8>`.
322///
323/// Subject values are written via `write_iri_term` (which accounts for the
324/// MSB / did:q42 flag).  Object values additionally check bits 60-62 for
325/// inline-typed literals.
326pub fn format_ntriples_to<W: io::Write>(quins: &[NQuin], out: &mut W) -> io::Result<()> {
327    for q in quins {
328        write_ntriple_line(q, out)?;
329    }
330    Ok(())
331}
332
333/// N-Quads lines with graph context (zero-heap).
334pub fn format_nquads_to<W: io::Write>(quins: &[NQuin], out: &mut W) -> io::Result<()> {
335    for q in quins {
336        write_iri_term(q.subject, out)?;
337        out.write_all(b" ")?;
338        write_iri_term(q.predicate, out)?;
339        out.write_all(b" ")?;
340        write_object_term(q.object, out)?;
341        out.write_all(b" ")?;
342        write_iri_term(q.context, out)?;
343        out.write_all(b" .\n")?;
344    }
345    Ok(())
346}
347
348/// RDF-Star N-Triples line (`<<<...>>>` subject when virtual id).
349pub fn format_ntriples_star_to<W: io::Write>(quins: &[NQuin], out: &mut W) -> io::Result<()> {
350    for q in quins {
351        write_ntriples_star_line(q, out)?;
352    }
353    Ok(())
354}
355
356#[inline]
357fn write_ntriple_line<W: io::Write>(q: &NQuin, out: &mut W) -> io::Result<()> {
358    write_iri_term(q.subject, out)?;
359    out.write_all(b" ")?;
360    write_iri_term(q.predicate, out)?;
361    out.write_all(b" ")?;
362    write_object_term(q.object, out)?;
363    out.write_all(b" .\n")
364}
365
366#[inline]
367fn write_ntriples_star_line<W: io::Write>(q: &NQuin, out: &mut W) -> io::Result<()> {
368    if crate::rdf_star::is_virtual_id(q.subject) {
369        out.write_all(b"<<<")?;
370        write_iri_term(q.subject, out)?;
371        out.write_all(b">>> ")?;
372    } else {
373        write_iri_term(q.subject, out)?;
374        out.write_all(b" ")?;
375    }
376    write_iri_term(q.predicate, out)?;
377    out.write_all(b" ")?;
378    write_object_term(q.object, out)?;
379    out.write_all(b" .\n")
380}
381
382// ---------------------------------------------------------------------------
383// Tests
384// ---------------------------------------------------------------------------
385
386#[cfg(test)]
387mod tests {
388    use super::*;
389
390    fn quin(s: u64, p: u64, o: u64) -> NQuin {
391        NQuin {
392            subject: s,
393            predicate: p,
394            object: o,
395            context: 0,
396            metadata: 0,
397            parity: 0,
398        }
399    }
400
401    fn render(quins: &[NQuin]) -> String {
402        let mut buf = Vec::new();
403        format_ntriples_to(quins, &mut buf).unwrap();
404        String::from_utf8(buf).unwrap()
405    }
406
407    // --- resolve_hash -------------------------------------------------------
408
409    #[test]
410    fn known_hash_resolves_to_uri() {
411        let hash = crate::q_hash("Alice");
412        let result = resolve_hash(hash).unwrap();
413        assert_eq!(result, b"http://webizen.org/demo/Alice");
414    }
415
416    #[test]
417    fn unknown_hash_returns_none() {
418        assert!(resolve_hash(0xDEAD_BEEF_1234_5678).is_none());
419    }
420
421    #[test]
422    fn topological_pointer_returns_none() {
423        let ptr = crate::q_hash("z6Mk") | (1u64 << 63);
424        assert!(resolve_hash(ptr).is_none());
425    }
426
427    // --- write_iri_term / write_object_term ---------------------------------
428
429    #[test]
430    fn known_iri_rendered_with_angle_brackets() {
431        let mut buf = Vec::new();
432        write_iri_term(crate::q_hash("Alice"), &mut buf).unwrap();
433        assert_eq!(buf, b"<http://webizen.org/demo/Alice>");
434    }
435
436    #[test]
437    fn unknown_hash_fallback_is_hex() {
438        let mut buf = Vec::new();
439        write_iri_term(0x00_00_00_00_00_00_00_2A, &mut buf).unwrap();
440        // value 42 decimal = 0x2a hex
441        assert_eq!(buf, b"<quin:hash/000000000000002a>");
442    }
443
444    #[test]
445    fn topological_pointer_renders_as_did_q42_ptr() {
446        let val = 42u64 | (1u64 << 63);
447        let mut buf = Vec::new();
448        write_iri_term(val, &mut buf).unwrap();
449        let s = String::from_utf8(buf).unwrap();
450        assert!(s.starts_with("<did:q42:ptr/"), "got: {s}");
451    }
452
453    #[test]
454    fn inline_integer_object() {
455        let val = INLINE_TAG_INTEGER | 99;
456        let mut buf = Vec::new();
457        write_object_term(val, &mut buf).unwrap();
458        let s = String::from_utf8(buf).unwrap();
459        assert_eq!(s, "\"99\"^^<http://www.w3.org/2001/XMLSchema#integer>");
460    }
461
462    #[test]
463    fn inline_boolean_true() {
464        let val = INLINE_TAG_BOOLEAN | 1;
465        let mut buf = Vec::new();
466        write_object_term(val, &mut buf).unwrap();
467        assert_eq!(
468            String::from_utf8(buf).unwrap(),
469            "\"true\"^^<http://www.w3.org/2001/XMLSchema#boolean>"
470        );
471    }
472
473    #[test]
474    fn inline_boolean_false() {
475        let val = INLINE_TAG_BOOLEAN | 0;
476        let mut buf = Vec::new();
477        write_object_term(val, &mut buf).unwrap();
478        assert_eq!(
479            String::from_utf8(buf).unwrap(),
480            "\"false\"^^<http://www.w3.org/2001/XMLSchema#boolean>"
481        );
482    }
483
484    #[test]
485    fn inline_decimal_object() {
486        // Encode 3.141592 → raw = 3_141_592
487        let val = INLINE_TAG_DECIMAL | 3_141_592u64;
488        let mut buf = Vec::new();
489        write_object_term(val, &mut buf).unwrap();
490        assert_eq!(
491            String::from_utf8(buf).unwrap(),
492            "\"3.141592\"^^<http://www.w3.org/2001/XMLSchema#decimal>"
493        );
494    }
495
496    #[test]
497    fn inline_float_object() {
498        // Computed f32 values carry the 0b101 FLOAT tag (formerly squatted on INTEGER).
499        let val = INLINE_TAG_FLOAT | (3.5f32.to_bits() as u64);
500        let mut buf = Vec::new();
501        write_object_term(val, &mut buf).unwrap();
502        assert_eq!(
503            String::from_utf8(buf).unwrap(),
504            "\"3.5\"^^<http://www.w3.org/2001/XMLSchema#float>"
505        );
506        // Round-trips through the FrameLayout packer too.
507        let packed = crate::frame_layout::pack_float_object(3.5);
508        assert_eq!(
509            packed & crate::frame_layout::INLINE_TAG_MASK,
510            INLINE_TAG_FLOAT
511        );
512    }
513
514    #[test]
515    fn inline_negative_integer_object() {
516        let num = -42i64;
517        let unsigned = (num as u64) & INLINE_VALUE_MASK;
518        let val = INLINE_TAG_INTEGER | unsigned;
519        let mut buf = Vec::new();
520        write_object_term(val, &mut buf).unwrap();
521        assert_eq!(
522            String::from_utf8(buf).unwrap(),
523            "\"-42\"^^<http://www.w3.org/2001/XMLSchema#integer>"
524        );
525    }
526
527    #[test]
528    fn inline_negative_decimal_object() {
529        let num_f64 = -3.141592f64;
530        let num = (num_f64 * 1_000_000.0).round() as i64;
531        let unsigned = (num as u64) & INLINE_VALUE_MASK;
532        let val = INLINE_TAG_DECIMAL | unsigned;
533        let mut buf = Vec::new();
534        write_object_term(val, &mut buf).unwrap();
535        assert_eq!(
536            String::from_utf8(buf).unwrap(),
537            "\"-3.141592\"^^<http://www.w3.org/2001/XMLSchema#decimal>"
538        );
539    }
540
541    // --- format_ntriples_to -------------------------------------------------
542
543    #[test]
544    fn empty_slice_writes_nothing() {
545        assert_eq!(render(&[]), "");
546    }
547
548    #[test]
549    fn known_terms_resolve_to_iris() {
550        let q = quin(
551            crate::q_hash("Alice"),
552            crate::q_hash("knows"),
553            crate::q_hash("Bob"),
554        );
555        let out = render(&[q]);
556        assert!(
557            out.contains("<http://webizen.org/demo/Alice>"),
558            "got: {out}"
559        );
560        assert!(out.contains("<http://schema.org/knows>"), "got: {out}");
561        assert!(out.contains("<http://webizen.org/demo/Bob>"), "got: {out}");
562        assert!(out.ends_with(" .\n"));
563    }
564
565    #[test]
566    fn unknown_terms_use_hex_fallback() {
567        let q = quin(1, 2, 3);
568        let out = render(&[q]);
569        assert!(out.contains("<quin:hash/0000000000000001>"), "got: {out}");
570        assert!(out.contains("<quin:hash/0000000000000002>"), "got: {out}");
571        assert!(out.contains("<quin:hash/0000000000000003>"), "got: {out}");
572    }
573
574    #[test]
575    fn multiple_quins_produce_multiple_lines() {
576        let qs = [quin(1, 2, 3), quin(4, 5, 6)];
577        let out = render(&qs);
578        assert_eq!(out.lines().count(), 2);
579    }
580
581    #[test]
582    fn subject_msb_renders_as_topological_pointer() {
583        // A subject with MSB=1 is a did:q42 coordinate — not a nested-hash lookup.
584        let q = quin((1u64 << 63) | 42, 2, 3);
585        let out = render(&[q]);
586        assert!(out.starts_with("<did:q42:ptr/"), "got: {out}");
587    }
588}