1use std::fmt;
27
28#[inline]
32fn decode_utf8_char(bytes: &[u8], i: usize) -> (char, usize) {
33 let b = bytes[i];
34 if b < 0x80 {
35 return (b as char, 1);
36 }
37 let remaining = &bytes[i..];
38 match std::str::from_utf8(remaining) {
39 Ok(s) => {
40 let c = s.chars().next().unwrap_or(b as char);
41 (c, c.len_utf8())
42 }
43 Err(e) => {
44 let valid_len = e.valid_up_to();
45 if valid_len > 0 {
46 let s = std::str::from_utf8(&remaining[..valid_len]).unwrap();
47 let c = s.chars().next().unwrap();
48 (c, c.len_utf8())
49 } else {
50 (b as char, 1)
51 }
52 }
53 }
54}
55
56pub const MAX_STACK_TRIPLES: usize = 8;
59pub const MAX_PARSE_BRACE_DEPTH: usize = 16;
62pub const MAX_PARSE_STATEMENTS: usize = 65_536;
64
65#[derive(Debug, Clone, Copy, PartialEq)]
66pub enum Term<'a> {
67 Uri(&'a str),
68 Variable(&'a str),
69 Literal(&'a str),
70 Formula(&'a str),
74}
75
76#[derive(Debug, Clone, Copy, PartialEq)]
77pub struct Triple<'a> {
78 pub subject: Term<'a>,
79 pub predicate: Term<'a>,
80 pub object: Term<'a>,
81}
82
83#[derive(Debug, Clone, PartialEq)]
86pub struct Formula<'a> {
87 pub triples: Vec<Triple<'a>>,
88}
89
90#[derive(Debug, Clone, Copy, PartialEq)]
92pub struct StackFormula<'a> {
93 pub triples: [Triple<'a>; MAX_STACK_TRIPLES],
94 pub len: usize,
95}
96
97impl<'a> StackFormula<'a> {
98 #[inline]
99 pub fn new() -> Self {
100 Self {
101 triples: [empty_triple(); MAX_STACK_TRIPLES],
102 len: 0,
103 }
104 }
105 #[inline]
107 pub fn as_slice(&self) -> &[Triple<'a>] {
108 &self.triples[..self.len]
109 }
110}
111
112impl Default for StackFormula<'_> {
113 fn default() -> Self {
114 Self::new()
115 }
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub enum RuleType {
120 Strict,
121 Defeasible,
122 Defeater,
123 Linear,
124}
125
126#[derive(Debug, Clone, PartialEq)]
128pub struct Rule<'a> {
129 pub id: Option<&'a str>,
130 pub rule_type: RuleType,
131 pub weight: Option<f32>,
132 pub premise: Formula<'a>,
133 pub conclusion: Formula<'a>,
134}
135
136#[derive(Debug, Clone, Copy, PartialEq)]
138pub struct StackRule<'a> {
139 pub id: Option<&'a str>,
140 pub rule_type: RuleType,
141 pub weight: Option<f32>,
142 pub premise: StackFormula<'a>,
143 pub conclusion: StackFormula<'a>,
144}
145
146#[derive(Debug)]
148pub enum N3Event<'a> {
149 StaticTriple(Triple<'a>),
150 LogicRule(Rule<'a>),
151 AspBlock(&'a str),
152 DiffuseBlock(&'a str),
153}
154
155#[derive(Debug, Clone, Copy)]
158pub enum StackEvent<'a> {
159 StaticTriple(Triple<'a>),
160 LogicRule(StackRule<'a>),
161 AspBlock(&'a str),
162 DiffuseBlock(&'a str),
163}
164
165#[derive(Debug, Clone)]
166pub struct N3ParserError(pub &'static str);
167
168impl fmt::Display for N3ParserError {
169 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170 write!(f, "{}", self.0)
171 }
172}
173
174impl std::error::Error for N3ParserError {}
175
176#[inline]
177fn empty_triple<'a>() -> Triple<'a> {
178 Triple {
179 subject: Term::Uri(""),
180 predicate: Term::Uri(""),
181 object: Term::Uri(""),
182 }
183}
184
185pub fn q_hash_formula(text: &str) -> u64 {
195 const OFFSET: u64 = 0xcbf29ce484222325;
196 const PRIME: u64 = 0x0000_0100_0000_01B3;
197 let mut h = OFFSET;
198 let mut pending_space = false;
201 let mut started = false;
202 for c in text.chars() {
203 if c.is_whitespace() {
204 if started {
205 pending_space = true;
206 }
207 } else {
208 if pending_space {
209 h ^= 0x20;
210 h = h.wrapping_mul(PRIME);
211 pending_space = false;
212 }
213 for &b in c.encode_utf8(&mut [0u8; 4]).as_bytes() {
215 h ^= b as u64;
216 h = h.wrapping_mul(PRIME);
217 }
218 started = true;
219 }
220 }
221 h
222}
223
224pub fn term_uri_hash(term: &Term<'_>) -> Option<u64> {
226 match term {
227 Term::Uri(uri) => Some(crate::q_hash(uri)),
228 Term::Literal(lit) => Some(crate::q_hash(lit)),
229 Term::Formula(s) => Some(q_hash_formula(s)),
230 Term::Variable(_) => None,
231 }
232}
233
234struct TripleTokenizer<'a> {
242 s: &'a str,
243 b: &'a [u8],
244 i: usize,
245}
246
247impl<'a> TripleTokenizer<'a> {
248 fn new(s: &'a str) -> Self {
249 Self {
250 s,
251 b: s.as_bytes(),
252 i: 0,
253 }
254 }
255}
256
257impl<'a> Iterator for TripleTokenizer<'a> {
258 type Item = &'a str;
259
260 fn next(&mut self) -> Option<&'a str> {
261 let len = self.b.len();
262 loop {
267 while self.i < len {
268 let (c, w) = decode_utf8_char(self.b, self.i);
269 if c.is_whitespace() {
270 self.i += w;
271 } else {
272 break;
273 }
274 }
275 if self.i < len && self.b[self.i] == b'#' {
276 while self.i < len && self.b[self.i] != b'\n' {
277 self.i += 1;
278 }
279 continue;
280 }
281 break;
282 }
283 if self.i >= len {
284 return None;
285 }
286 let start = self.i;
287 let c = self.b[self.i];
288
289 if c == b'{' {
291 let mut depth = 0i32;
292 while self.i < len {
293 match self.b[self.i] {
294 b'{' => depth += 1,
295 b'}' => {
296 depth -= 1;
297 if depth == 0 {
298 self.i += 1;
299 break;
300 }
301 }
302 _ => {}
303 }
304 self.i += 1;
305 }
306 return Some(&self.s[start..self.i]);
307 }
308
309 if c == b'"' {
311 self.i += 1;
312 while self.i < len && self.b[self.i] != b'"' {
313 self.i += 1;
314 }
315 if self.i < len {
316 self.i += 1; }
318 return Some(&self.s[start..self.i]);
319 }
320
321 if c == b';' || c == b',' || c == b'.' {
323 self.i += 1;
324 return Some(&self.s[start..self.i]);
325 }
326
327 while self.i < len {
329 let d = self.b[self.i];
330 if d == b'.' {
331 let prev_digit = self.i > start && self.b[self.i - 1].is_ascii_digit();
333 let next_digit = self.i + 1 < len && self.b[self.i + 1].is_ascii_digit();
334 if prev_digit && next_digit {
335 self.i += 1;
336 continue;
337 }
338 break;
339 }
340 if d.is_ascii_whitespace() || d == b';' || d == b',' || d == b'{' || d == b'"' {
341 break;
342 }
343 if (d & 0x80) != 0 {
345 let (ch, _) = decode_utf8_char(self.b, self.i);
346 if ch.is_whitespace() {
347 break;
348 }
349 }
350 self.i += 1;
351 }
352 Some(&self.s[start..self.i])
353 }
354}
355
356fn for_each_triple<'a>(
363 block: &'a str,
364 mut emit: impl FnMut(Triple<'a>) -> Result<bool, N3ParserError>,
365) -> Result<(), N3ParserError> {
366 let mut s = block.trim();
367 if s.starts_with('{') && s.ends_with('}') {
368 s = s[1..s.len() - 1].trim();
369 }
370
371 let mut subject: Option<Term<'a>> = None;
372 let mut predicate: Option<Term<'a>> = None;
373
374 for tok in TripleTokenizer::new(s) {
375 match tok {
376 "." => {
377 subject = None;
378 predicate = None;
379 }
380 ";" => {
381 predicate = None; }
383 "," => {
384 }
386 node => {
387 let term = N3Parser::parse_term(node);
388 if subject.is_none() {
389 subject = Some(term);
390 } else if predicate.is_none() {
391 predicate = Some(term);
392 } else {
393 let triple = Triple {
394 subject: subject.unwrap(),
395 predicate: predicate.unwrap(),
396 object: term,
397 };
398 if !emit(triple)? {
399 return Ok(());
400 }
401 }
405 }
406 }
407 }
408 Ok(())
409}
410
411pub struct N3Parser<'a> {
413 text: &'a str,
414}
415
416impl<'a> N3Parser<'a> {
417 pub fn new(text: &'a str) -> Self {
418 N3Parser { text }
419 }
420
421 pub fn parse_all<F>(&mut self, callback: F) -> Result<(), N3ParserError>
424 where
425 F: FnMut(N3Event<'a>) -> Result<(), N3ParserError>,
426 {
427 self.scan(callback, dispatch_statement_heap, emit_block_heap)
428 }
429
430 pub fn parse_all_zero_heap<F>(&mut self, callback: F) -> Result<(), N3ParserError>
432 where
433 F: FnMut(StackEvent<'a>) -> Result<(), N3ParserError>,
434 {
435 self.scan(callback, dispatch_statement_stack, emit_block_stack)
436 }
437
438 fn scan<F>(
444 &mut self,
445 mut callback: F,
446 dispatch: fn(&'a str, &mut F) -> Result<(), N3ParserError>,
447 emit_block: fn(BlockKind, &'a str, &mut F) -> Result<(), N3ParserError>,
448 ) -> Result<(), N3ParserError> {
449 let bytes = self.text.as_bytes();
450 let len = bytes.len();
451
452 let mut i = 0;
453 let mut stmt_start = 0;
454 let mut brace_depth: i32 = 0;
455 let mut in_comment = false;
456 let mut in_string = false;
457 let mut statements = 0usize;
458
459 while i < len {
460 let c = bytes[i] as char;
461
462 if in_comment {
463 if c == '\n' {
464 in_comment = false;
465 }
466 i += 1;
467 continue;
468 }
469
470 if in_string {
475 if c == '"' {
476 in_string = false;
477 }
478 i += 1;
479 continue;
480 }
481 if c == '"' {
482 in_string = true;
483 i += 1;
484 continue;
485 }
486
487 if c == '#' {
488 if self.text[i..].starts_with("#asp {") {
489 let end = self.text[i..].find('}').unwrap_or(self.text[i..].len());
490 emit_block(
491 BlockKind::Asp,
492 self.text[i + 6..i + end].trim(),
493 &mut callback,
494 )?;
495 i += end + 1;
496 stmt_start = i;
497 continue;
498 } else {
499 in_comment = true;
500 i += 1;
501 continue;
502 }
503 }
504
505 if c == 'q' && self.text[i..].starts_with("qualia:diffuse {") {
509 let end = self.text[i..].find('}').unwrap_or(self.text[i..].len());
510 emit_block(
511 BlockKind::Diffuse,
512 self.text[i + 16..i + end].trim(),
513 &mut callback,
514 )?;
515 i += end + 1;
516 stmt_start = i;
517 continue;
518 }
519
520 if c == '{' {
521 brace_depth += 1;
522 if brace_depth as usize > MAX_PARSE_BRACE_DEPTH {
523 return Err(N3ParserError(
524 "N3 brace nesting exceeds MAX_PARSE_BRACE_DEPTH",
525 ));
526 }
527 }
528 if c == '}' {
529 brace_depth -= 1;
530 if brace_depth < 0 {
531 return Err(N3ParserError("unbalanced '}' in N3 input"));
532 }
533 }
534
535 if c == '.' && brace_depth <= 0 {
536 let stmt = self.text[stmt_start..=i].trim();
537 statements += 1;
538 if statements > MAX_PARSE_STATEMENTS {
539 return Err(N3ParserError(
540 "N3 statement count exceeds MAX_PARSE_STATEMENTS",
541 ));
542 }
543 dispatch(stmt, &mut callback)?;
544 stmt_start = i + 1;
545 }
546
547 i += 1;
548 }
549
550 if brace_depth != 0 {
551 return Err(N3ParserError("unbalanced '{' in N3 input"));
552 }
553
554 let rem = self.text[stmt_start..].trim();
555 if !rem.is_empty() && !rem.starts_with('@') && !rem.starts_with('#') {
556 dispatch(rem, &mut callback)?;
557 }
558
559 Ok(())
560 }
561
562 pub fn parse_triples_into(block: &'a str, out: &mut [Triple<'a>]) -> usize {
567 let mut n = 0usize;
568 let _ = for_each_triple(block, |t| {
569 if n < out.len() {
570 out[n] = t;
571 n += 1;
572 Ok(n < out.len())
573 } else {
574 Ok(false)
575 }
576 });
577 n
578 }
579
580 pub fn parse_rule_zero_heap(line: &'a str) -> Option<StackRule<'a>> {
582 let (id, weight, rule_type, premise_str, conclusion_str) = split_rule(line)?;
583 let mut premise = StackFormula::new();
584 premise.len = Self::parse_triples_into(premise_str, &mut premise.triples);
585 let mut conclusion = StackFormula::new();
586 conclusion.len = Self::parse_triples_into(conclusion_str, &mut conclusion.triples);
587 Some(StackRule {
588 id,
589 rule_type,
590 weight,
591 premise,
592 conclusion,
593 })
594 }
595
596 fn parse_rule(line: &'a str) -> Option<Rule<'a>> {
599 let (id, weight, rule_type, premise_str, conclusion_str) = split_rule(line)?;
600 Some(Rule {
601 id,
602 rule_type,
603 weight,
604 premise: Formula {
605 triples: Self::parse_formula_triples(premise_str),
606 },
607 conclusion: Formula {
608 triples: Self::parse_formula_triples(conclusion_str),
609 },
610 })
611 }
612
613 fn parse_formula_triples(block: &'a str) -> Vec<Triple<'a>> {
614 let mut triples = Vec::new();
615 let _ = for_each_triple(block, |t| {
616 triples.push(t);
617 Ok(true)
618 });
619 triples
620 }
621
622 fn parse_term(s: &'a str) -> Term<'a> {
623 let s = s.trim();
624 if s.is_empty() {
625 return Term::Uri("");
626 }
627 if let Some(rest) = s.strip_prefix('?') {
632 let _ = rest;
633 return Term::Variable(s);
634 }
635 if s.starts_with('{') {
636 let inner = s
637 .strip_prefix('{')
638 .and_then(|x| x.strip_suffix('}'))
639 .unwrap_or(s)
640 .trim();
641 return Term::Formula(inner);
642 }
643 if s.starts_with('"') {
644 return Term::Literal(s.trim_matches('"'));
646 }
647 if s.parse::<f64>().is_ok() {
648 return Term::Literal(s);
649 }
650 Term::Uri(s)
651 }
652}
653
654fn looks_like_rule(s: &str) -> bool {
656 s.contains("=>") || s.contains("~>") || s.contains("^>") || s.contains(" -o ")
657}
658
659fn split_rule(line: &str) -> Option<(Option<&str>, Option<f32>, RuleType, &str, &str)> {
662 let mut clean = line.trim();
663 let mut id = None;
664 let mut weight = None;
665
666 if clean.starts_with('[') {
667 if let Some(end) = clean.find(']') {
668 id = Some(clean[1..end].trim());
669 clean = clean[end + 1..].trim();
670 }
671 }
672 if clean.starts_with('(') {
673 if let Some(end) = clean.find(')') {
674 if let Ok(w) = clean[1..end].trim().parse::<f32>() {
675 weight = Some(w);
676 }
677 clean = clean[end + 1..].trim();
678 }
679 }
680
681 let (rule_type, arrow_len, arrow_idx) = if let Some(idx) = clean.find("=>") {
682 (RuleType::Strict, 2, idx)
683 } else if let Some(idx) = clean.find("~>") {
684 (RuleType::Defeasible, 2, idx)
685 } else if let Some(idx) = clean.find("^>") {
686 (RuleType::Defeater, 2, idx)
687 } else if let Some(idx) = clean.find(" -o ") {
688 (RuleType::Linear, 4, idx)
689 } else {
690 return None;
691 };
692
693 let premise = clean[..arrow_idx].trim();
694 let conclusion = clean[arrow_idx + arrow_len..].trim().trim_end_matches('.');
695 Some((id, weight, rule_type, premise, conclusion))
696}
697
698fn trim_leading_comment_lines(mut s: &str) -> &str {
699 loop {
700 let trimmed = s.trim_start();
701 let Some(rest) = trimmed.strip_prefix('#') else {
702 return trimmed;
703 };
704 let Some(newline) = rest.find('\n') else {
705 return "";
706 };
707 s = &rest[newline + 1..];
708 }
709}
710
711enum BlockKind {
714 Asp,
715 Diffuse,
716}
717
718fn emit_block_heap<'a, F>(
719 kind: BlockKind,
720 body: &'a str,
721 callback: &mut F,
722) -> Result<(), N3ParserError>
723where
724 F: FnMut(N3Event<'a>) -> Result<(), N3ParserError>,
725{
726 match kind {
727 BlockKind::Asp => callback(N3Event::AspBlock(body)),
728 BlockKind::Diffuse => callback(N3Event::DiffuseBlock(body)),
729 }
730}
731
732fn emit_block_stack<'a, F>(
733 kind: BlockKind,
734 body: &'a str,
735 callback: &mut F,
736) -> Result<(), N3ParserError>
737where
738 F: FnMut(StackEvent<'a>) -> Result<(), N3ParserError>,
739{
740 match kind {
741 BlockKind::Asp => callback(StackEvent::AspBlock(body)),
742 BlockKind::Diffuse => callback(StackEvent::DiffuseBlock(body)),
743 }
744}
745
746fn dispatch_statement_heap<'a, F>(stmt: &'a str, callback: &mut F) -> Result<(), N3ParserError>
747where
748 F: FnMut(N3Event<'a>) -> Result<(), N3ParserError>,
749{
750 let s = trim_leading_comment_lines(stmt);
751 if s.is_empty() {
752 return Ok(());
753 }
754 if looks_like_rule(s) {
755 if let Some(rule) = N3Parser::parse_rule(s) {
756 return callback(N3Event::LogicRule(rule));
757 }
758 }
759 let triples = N3Parser::parse_formula_triples(s.trim_end_matches('.'));
760 for triple in triples {
761 callback(N3Event::StaticTriple(triple))?;
762 }
763 Ok(())
764}
765
766fn dispatch_statement_stack<'a, F>(stmt: &'a str, callback: &mut F) -> Result<(), N3ParserError>
767where
768 F: FnMut(StackEvent<'a>) -> Result<(), N3ParserError>,
769{
770 let s = trim_leading_comment_lines(stmt);
771 if s.is_empty() {
772 return Ok(());
773 }
774 if looks_like_rule(s) {
775 if let Some(rule) = N3Parser::parse_rule_zero_heap(s) {
776 return callback(StackEvent::LogicRule(rule));
777 }
778 }
779 let mut err: Option<N3ParserError> = None;
780 for_each_triple(s.trim_end_matches('.'), |t| {
781 match callback(StackEvent::StaticTriple(t)) {
782 Ok(()) => Ok(true),
783 Err(e) => {
784 err = Some(e);
785 Ok(false)
786 }
787 }
788 })?;
789 if let Some(e) = err {
790 return Err(e);
791 }
792 Ok(())
793}
794
795#[cfg(test)]
796mod tests {
797 use super::*;
798
799 #[test]
802 fn keyword_a_is_the_engine_type_token() {
803 let mut buf = [empty_triple(); MAX_STACK_TRIPLES];
807 let n = N3Parser::parse_triples_into(":x a :Y .", &mut buf);
808 assert_eq!(n, 1);
809 assert_eq!(buf[0].predicate, Term::Uri("a"));
810 assert_eq!(buf[0].subject, Term::Uri(":x"));
811 assert_eq!(buf[0].object, Term::Uri(":Y"));
812 }
813
814 #[test]
815 fn object_lists_and_predicate_lists() {
816 let mut buf = [empty_triple(); MAX_STACK_TRIPLES];
817 let n = N3Parser::parse_triples_into(":x :p :a, :b ; :q :c .", &mut buf);
819 assert_eq!(n, 3);
820 assert_eq!(
821 buf[0],
822 Triple {
823 subject: Term::Uri(":x"),
824 predicate: Term::Uri(":p"),
825 object: Term::Uri(":a")
826 }
827 );
828 assert_eq!(
829 buf[1],
830 Triple {
831 subject: Term::Uri(":x"),
832 predicate: Term::Uri(":p"),
833 object: Term::Uri(":b")
834 }
835 );
836 assert_eq!(
837 buf[2],
838 Triple {
839 subject: Term::Uri(":x"),
840 predicate: Term::Uri(":q"),
841 object: Term::Uri(":c")
842 }
843 );
844 }
845
846 #[test]
847 fn leading_comment_before_rule_preserves_premise() {
848 let text = r#"
849# (G1) Corporate-capture guard.
850{ ?c a values:CorporatePerson ; values:claims ?r .
851 ?r a values:Right ; values:heldBy values:NaturalPerson
852} => { ?c values:flag values:PersonhoodCategoryError } .
853"#;
854 let mut rules = Vec::new();
855 let mut parser = N3Parser::new(text);
856 parser
857 .parse_all(|ev| {
858 if let N3Event::LogicRule(rule) = ev {
859 rules.push(rule);
860 }
861 Ok(())
862 })
863 .unwrap();
864
865 assert_eq!(rules.len(), 1);
866 assert_eq!(rules[0].premise.triples.len(), 4);
867 assert_eq!(rules[0].conclusion.triples.len(), 1);
868 assert_eq!(rules[0].premise.triples[0].subject, Term::Variable("?c"));
869 assert_eq!(rules[0].premise.triples[0].predicate, Term::Uri("a"));
870 assert_eq!(
871 rules[0].premise.triples[0].object,
872 Term::Uri("values:CorporatePerson")
873 );
874 }
875
876 #[test]
877 fn decimal_literal_is_one_token() {
878 let mut buf = [empty_triple(); MAX_STACK_TRIPLES];
879 let n = N3Parser::parse_triples_into(":x :p 3.14 .", &mut buf);
880 assert_eq!(n, 1);
881 assert_eq!(buf[0].object, Term::Literal("3.14"));
882 }
883
884 #[test]
885 fn parses_implication_rule_zero_heap() {
886 let rule = N3Parser::parse_rule_zero_heap("{ ?x a ?y } => { ?y a ?z } .").unwrap();
887 assert_eq!(rule.rule_type, RuleType::Strict);
888 assert_eq!(rule.premise.len, 1);
889 assert_eq!(rule.conclusion.len, 1);
890 let p = rule.premise.triples[0];
891 assert_eq!(p.subject, Term::Variable("?x"));
892 assert_eq!(p.predicate, Term::Uri("a"));
893 assert_eq!(p.object, Term::Variable("?y"));
894 }
895
896 #[test]
899 fn quoted_formula_term_is_captured() {
900 let mut buf = [empty_triple(); MAX_STACK_TRIPLES];
901 let n = N3Parser::parse_triples_into("{ :alice :says :x } :trustedBy :bob .", &mut buf);
902 assert_eq!(n, 1);
903 match buf[0].subject {
904 Term::Formula(body) => assert!(body.contains(":alice :says :x")),
905 other => panic!("expected a quoted formula subject, got {other:?}"),
906 }
907 assert_eq!(buf[0].predicate, Term::Uri(":trustedBy"));
908 assert_eq!(buf[0].object, Term::Uri(":bob"));
909 }
910
911 #[test]
912 fn formula_with_internal_period_is_one_term() {
913 let mut buf = [empty_triple(); MAX_STACK_TRIPLES];
914 let n = N3Parser::parse_triples_into("{ :a :b :c . :d :e :f } :g :h .", &mut buf);
915 assert_eq!(n, 1);
916 assert!(matches!(buf[0].subject, Term::Formula(_)));
917 assert_eq!(buf[0].object, Term::Uri(":h"));
918 }
919
920 #[test]
921 fn reification_handle_is_whitespace_canonical() {
922 assert_eq!(q_hash_formula(":a :b :c"), q_hash_formula(" :a :b :c "));
924 assert_ne!(q_hash_formula(":a :b :c"), q_hash_formula(":a :b :d"));
925 }
926
927 #[test]
930 fn rejects_excessive_brace_nesting() {
931 let deep = "{".repeat(MAX_PARSE_BRACE_DEPTH + 2);
932 let mut parser = N3Parser::new(&deep);
933 let r = parser.parse_all(|_| Ok(()));
934 assert!(r.is_err());
935 }
936
937 #[test]
938 fn rejects_unbalanced_braces() {
939 let mut parser = N3Parser::new("} :a :b :c .");
940 assert!(parser.parse_all(|_| Ok(())).is_err());
941 }
942
943 #[test]
944 fn parses_multibyte_utf8_without_panicking() {
945 let doc = ":x :label \"café — déjà ➜ vu\" .\n\
948 <http://例え.example/Ω> :note \":naïve\" .\n";
949 let mut parser = N3Parser::new(doc);
950 let mut n = 0usize;
951 parser
952 .parse_all(|ev| {
953 if let N3Event::StaticTriple(_) = ev {
954 n += 1;
955 }
956 Ok(())
957 })
958 .unwrap();
959 assert!(n >= 2, "expected both UTF-8 triples, got {n}");
960 }
961
962 #[test]
965 fn parse_all_zero_heap_allocates_nothing() {
966 let doc = "\
967 :alice a :Person .\n\
968 :alice :knows :bob, :carol .\n\
969 { ?x a :Person } => { ?x a :Agent } .\n\
970 { :alice :says :hi } :assertedBy :alice .\n";
971 let _profiler = dhat::Profiler::builder().testing().build();
972
973 let mut parser = N3Parser::new(doc);
974 let mut triples = 0usize;
975 let mut rules = 0usize;
976 parser
977 .parse_all_zero_heap(|ev| {
978 match ev {
979 StackEvent::StaticTriple(_) => triples += 1,
980 StackEvent::LogicRule(_) => rules += 1,
981 _ => {}
982 }
983 Ok(())
984 })
985 .unwrap();
986
987 let stats = dhat::HeapStats::get();
988 assert_eq!(
989 stats.curr_blocks, 0,
990 "parse_all_zero_heap must not allocate"
991 );
992 assert_eq!(stats.curr_bytes, 0);
993 assert!(triples >= 4, "expected the static + object-list triples");
994 assert_eq!(rules, 1);
995 }
996
997 #[test]
1000 fn implication_compiles_to_sentinel_bytecode() {
1001 use crate::modalities::logic::n3_compiler::{
1002 compile_rule_to_opcodes, compile_rule_to_zero_heap, MAX_COMPILED_OPCODES,
1003 };
1004 use crate::webizen::SlgOpcode;
1005
1006 let mut parser = N3Parser::new("{ ?x a ?y } => { ?y a ?z } .");
1008 let mut compiled_ok = false;
1009 parser
1010 .parse_all(|ev| {
1011 if let N3Event::LogicRule(rule) = ev {
1012 let compiled = compile_rule_to_zero_heap(&rule);
1013 let mut ops = [SlgOpcode::Call; MAX_COMPILED_OPCODES];
1014 let n = compile_rule_to_opcodes(&compiled, &mut ops).unwrap();
1015 assert!(n >= 3);
1016 assert_eq!(ops[0], SlgOpcode::Unify);
1017 compiled_ok = true;
1018 }
1019 Ok(())
1020 })
1021 .unwrap();
1022 assert!(
1023 compiled_ok,
1024 "the implication rule did not reach the compiler"
1025 );
1026 }
1027}