qualia_core_db/query/mini_parser.rs
1//! Zero-allocation N-Triples pattern compiler.
2//!
3//! Accepts a single N-Triples pattern line such as
4//! `<http://example.org/Alice> <schema:knows> ?who .`
5//! and compiles it into a flat bytecode program that `webizen::bytecode::execute_program`
6//! can run directly against a `&[NQuin]` slice without any heap allocation.
7//!
8//! # Bytecode encoding
9//! Each instruction occupies either 1 or 9 bytes:
10//!
11//! | Opcode byte | Operand (bytes 1-8) | Meaning |
12//! |-------------|---------------------|-------------------------------|
13//! | 0x01 | u64 LE hash | MatchSubject(hash) |
14//! | 0x02 | u64 LE hash | MatchPredicate(hash) |
15//! | 0x03 | u64 LE hash | MatchObject(hash) |
16//! | 0x04 | — | HaltIfFalse (skip this Quin) |
17//! | 0x00 | — | End / accept this Quin |
18//!
19//! Variable tokens (`?name`) produce no instruction — the corresponding
20//! Quin vector is treated as a wildcard.
21
22pub const OP_END: u8 = 0x00;
23pub const OP_MATCH_SUBJECT: u8 = 0x01;
24pub const OP_MATCH_PREDICATE: u8 = 0x02;
25pub const OP_MATCH_OBJECT: u8 = 0x03;
26pub const OP_HALT_IF_FALSE: u8 = 0x04;
27
28// Sentinel Deontic Logic ISA Extensions
29pub const OP_EVAL_PERMIT: u8 = 0x50;
30pub const OP_EVAL_OBLIGATE: u8 = 0x51;
31pub const OP_EVAL_FORBID: u8 = 0x52;
32pub const OP_HALT_VIOLATION: u8 = 0x53;
33
34#[derive(Debug, PartialEq)]
35pub enum ParseError {
36 /// The query bytes are not valid UTF-8 or contain no recognisable tokens.
37 Malformed,
38 /// The compiled program would overflow the 1 KiB fixed program buffer.
39 ProgramTooLarge,
40}
41
42/// Compile a single N-Triples pattern line into `program`.
43///
44/// Returns the number of bytes written to `program` on success.
45/// Wildcards (`?var`) are silently elided; bound terms are hashed with FNV-1a
46/// so the VM can compare directly against `NQuin` field values.
47pub fn compile_ntriples_to_bytecode(
48 query: &[u8],
49 program: &mut [u8; 1024],
50) -> Result<usize, ParseError> {
51 let text = core::str::from_utf8(query).map_err(|_| ParseError::Malformed)?;
52
53 let mut pos = 0usize; // write cursor into program
54 let mut token_idx = 0usize; // 0 = subject, 1 = predicate, 2 = object
55
56 for token in text.split_whitespace() {
57 if token == "." {
58 break;
59 }
60 if token_idx > 2 {
61 break;
62 }
63
64 let opcode = match token_idx {
65 0 => OP_MATCH_SUBJECT,
66 1 => OP_MATCH_PREDICATE,
67 _ => OP_MATCH_OBJECT,
68 };
69 token_idx += 1;
70
71 // Variables are wildcards — no constraint emitted.
72 if token.starts_with('?') {
73 continue;
74 }
75
76 let hash = hash_token(token);
77
78 // 9 bytes for the match instruction + 1 byte for HaltIfFalse + 1 byte reserved for END.
79 if pos + 11 > program.len() {
80 return Err(ParseError::ProgramTooLarge);
81 }
82
83 program[pos] = opcode;
84 program[pos + 1..pos + 9].copy_from_slice(&hash.to_le_bytes());
85 pos += 9;
86
87 program[pos] = OP_HALT_IF_FALSE;
88 pos += 1;
89 }
90
91 if token_idx == 0 {
92 return Err(ParseError::Malformed);
93 }
94
95 program[pos] = OP_END;
96 pos += 1;
97 Ok(pos)
98}
99
100/// Resolve a single N-Triples token to its 64-bit Quin vector value.
101///
102/// Routing rules (applied after bracket-stripping):
103/// 1. If the inner URI starts with `did:q42:` → route through
104/// [`crate::identifier::parse_did_q42`], which sets bit 63 to mark the
105/// result as a topological hardware pointer.
106/// 2. All other URIs and literals → standard `q_hash` (bit 63 = 0).
107///
108/// This function is `pub` so the CLI ingest pipeline and the WASM bridge can
109/// hash tokens with identical logic without duplicating the dispatch table.
110#[inline(always)]
111pub fn hash_token(token: &str) -> u64 {
112 // Strip angle-bracket URI delimiters and double-quote literal delimiters.
113 // For literals we scan for the first unescaped closing `"` so that
114 // language-tagged (`"val"@lang`) and datatype-tagged (`"val"^^<dt>`)
115 // literals are treated identically to plain `"val"` — only the value
116 // itself is hashed. This matches hash.js `hashToken` exactly.
117 let inner = if token.starts_with('<') && token.ends_with('>') {
118 &token[1..token.len() - 1]
119 } else if token.starts_with('"') {
120 let bytes = token.as_bytes();
121 let mut i = 1;
122 while i < bytes.len() {
123 if bytes[i] == b'\\' {
124 i += 2;
125 continue;
126 }
127 if bytes[i] == b'"' {
128 break;
129 }
130 i += 1;
131 }
132 &token[1..i]
133 } else {
134 token
135 };
136
137 // Route `did:q42:` coordinates through the identifier module so the MSB
138 // flag is applied, distinguishing them from plain dictionary hashes.
139 if inner.as_bytes().starts_with(b"did:q42:") {
140 if let Ok(pointer) = crate::identifier::parse_did_q42(inner.as_bytes()) {
141 return pointer;
142 }
143 // Malformed did:q42 URI — fall through to standard hash so the query
144 // can still execute; the Webizen VM will simply find no match.
145 }
146
147 crate::q_hash(inner)
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn compile_bound_triple() {
156 let mut prog = [0u8; 1024];
157 let n = compile_ntriples_to_bytecode(b"<Alice> <knows> <Bob> .", &mut prog).unwrap();
158 // 3 × (9 match + 1 halt) + 1 end = 31 bytes
159 assert_eq!(n, 31);
160 assert_eq!(prog[0], OP_MATCH_SUBJECT);
161 assert_eq!(prog[9], OP_HALT_IF_FALSE);
162 assert_eq!(prog[10], OP_MATCH_PREDICATE);
163 assert_eq!(prog[19], OP_HALT_IF_FALSE);
164 assert_eq!(prog[20], OP_MATCH_OBJECT);
165 assert_eq!(prog[29], OP_HALT_IF_FALSE);
166 assert_eq!(prog[30], OP_END);
167 }
168
169 #[test]
170 fn compile_wildcard_subject() {
171 let mut prog = [0u8; 1024];
172 let n = compile_ntriples_to_bytecode(b"?who <knows> <Bob> .", &mut prog).unwrap();
173 // Subject is wildcard → 2 × (9 + 1) + 1 = 21 bytes
174 assert_eq!(n, 21);
175 assert_eq!(prog[0], OP_MATCH_PREDICATE);
176 }
177
178 #[test]
179 fn compile_empty_fails() {
180 let mut prog = [0u8; 1024];
181 assert_eq!(
182 compile_ntriples_to_bytecode(b" ", &mut prog),
183 Err(ParseError::Malformed)
184 );
185 }
186
187 #[test]
188 fn hashes_strip_brackets() {
189 assert_eq!(hash_token("<Alice>"), hash_token("Alice"));
190 assert_eq!(hash_token("\"hello\""), hash_token("hello"));
191 }
192
193 #[test]
194 fn did_q42_sets_msb_in_compiled_bytecode() {
195 let mut prog = [0u8; 1024];
196 // The subject is a did:q42 coordinate; predicate and object are plain URIs.
197 compile_ntriples_to_bytecode(b"<did:q42:z6MkpTHR8VNs> <knows> <Bob> .", &mut prog).unwrap();
198
199 // Subject hash is in bytes 1–8 (immediately after OP_MATCH_SUBJECT).
200 let subject_hash = u64::from_le_bytes(prog[1..9].try_into().unwrap());
201 assert_eq!(
202 subject_hash >> 63,
203 1,
204 "did:q42 subject must have MSB set in compiled bytecode"
205 );
206
207 // The predicate hash must equal q_hash("knows") | (1<<63) when that hash
208 // already has MSB set, or just q_hash("knows") when it does not.
209 // The contract we verify here is only about the subject's MSB flag;
210 // FNV-1a can naturally produce MSB=1 for any input, so we do not assert
211 // MSB=0 for plain URI tokens.
212 let subject_expected = crate::q_hash("z6MkpTHR8VNs") | (1u64 << 63);
213 assert_eq!(subject_hash, subject_expected);
214 }
215
216 #[test]
217 fn did_q42_in_object_position() {
218 let mut prog = [0u8; 1024];
219 compile_ntriples_to_bytecode(b"?who <knows> <did:q42:z6MkAbCd> .", &mut prog).unwrap();
220 // With wildcard subject: OP_MATCH_PREDICATE at 0, object at 10.
221 let object_hash = u64::from_le_bytes(prog[11..19].try_into().unwrap());
222 assert_eq!(object_hash >> 63, 1, "did:q42 object must have MSB set");
223 }
224}