Skip to main content

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}