1use crate::NQuin;
36use std::io;
37
38pub const MSB_FLAG: u64 = 1u64 << 63;
43pub const INLINE_TAG_MASK: u64 = 0b111u64 << 60; pub const INLINE_TAG_INTEGER: u64 = 0b001u64 << 60;
45pub const INLINE_TAG_DECIMAL: u64 = 0b010u64 << 60;
46pub const INLINE_TAG_BOOLEAN: u64 = 0b011u64 << 60;
47pub const INLINE_TAG_FLOAT: u64 = 0b101u64 << 60;
51pub const TAG_EMBEDDED: u64 = 0b001u64 << 60;
53pub const TAG_WEBIZEN: u64 = 0b1000u64 << 60;
56pub const INLINE_VALUE_MASK: u64 = !(MSB_FLAG | INLINE_TAG_MASK);
58
59static 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
105pub 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 #[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
144pub fn resolve_hash(hash: u64) -> Option<&'static [u8]> {
154 if let Some(uri) = LEXICON.resolve(hash) {
158 return Some(uri);
159 }
160 if (hash & MSB_FLAG) != 0 {
161 return None; }
163 None
164}
165
166#[inline]
181pub(crate) fn write_iri_term<W: io::Write>(val: u64, out: &mut W) -> io::Result<()> {
182 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 if (val & MSB_FLAG) != 0 {
190 let ptr = val & !MSB_FLAG;
191 return write!(out, "<did:q42:ptr/{ptr:016x}>");
192 }
193 write!(out, "<quin:hash/{val:016x}>")
195}
196
197#[derive(Debug, Clone, Copy, PartialEq)]
201pub enum InlineLiteral {
202 Integer(i64),
204 Decimal(i64),
206 Boolean(bool),
208 Float(f32),
210}
211
212impl InlineLiteral {
213 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 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#[inline]
254pub fn classify_inline_literal(val: u64) -> Option<InlineLiteral> {
255 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#[inline]
290pub(crate) fn write_object_term<W: io::Write>(val: u64, out: &mut W) -> io::Result<()> {
291 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 if (val & MSB_FLAG) != 0 {
299 let ptr = val & !MSB_FLAG;
300 return write!(out, "<did:q42:ptr/{ptr:016x}>");
301 }
302 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
313pub 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
333pub 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
348pub 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#[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 #[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 #[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 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 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 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 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 #[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 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}