Skip to main content

verit_core/
idl.rs

1//! The `.vsc` schema IDL — a small text front-end that compiles to a
2//! [`Schema`]. It invents no wire semantics: it drives [`SchemaBuilder`], so
3//! the result is exactly the same canonical `VSC1` bytes (and 128-bit id) any
4//! other definition of the same schema produces. The IDL therefore cannot
5//! drift from the wire format — the id is the contract, and it is computed the
6//! same way regardless of how the schema was written.
7//!
8//! ## Grammar (v1)
9//!
10//! ```text
11//! // line comments
12//! struct Order {                 // sparse struct (presence bitmap)
13//!   1: id     u64                //   <field-id> : <name> <type>
14//!   2: item   string
15//!   3: qty    u32
16//!   4: tags   list<string>
17//!   5: origin Point              // reference to a named struct
18//!   6: level  Level              // reference to a named enum
19//! }
20//!
21//! dense struct Point { 1: x f64  2: y f64 }        // all fields mandatory, no bitmap
22//! packed struct Wide { 1: a u8  2: b u64 }         // present-only slots, popcount-indexed
23//!
24//! enum Level { 0: Debug  1: Info  2: Error }       // open, u32 repr
25//!
26//! root Order                     // the message root type (required, exactly one)
27//! ```
28//!
29//! Field IDs are explicit and are the evolution contract — never reuse an ID
30//! for a new meaning. Scalars: `bool`, `u8`..`u64`, `i8`..`i64`, `f32`, `f64`,
31//! `string`, `bytes`. Compound: `list<T>` and `map<K, V>` (both nestable) and a
32//! bare type name referring to a declared `struct`/`enum`. Map keys must be a
33//! `bool`, an integer, `string`, or an enum.
34
35use crate::error::{Error, Result};
36use crate::schema::{Dt, Schema, SchemaBuilder};
37use crate::value::Value;
38
39fn err(msg: impl Into<String>) -> Error {
40    Error::BadSchema(format!("idl: {}", msg.into()))
41}
42
43// ---------------------------------------------------------------------------
44// Tokenizer
45// ---------------------------------------------------------------------------
46
47#[derive(Debug, PartialEq)]
48enum Tok {
49    Ident(String), // keywords and names both arrive as idents
50    Int(u64),
51    LBrace,
52    RBrace,
53    Lt,
54    Gt,
55    Colon,
56    Eq,
57    Minus,
58}
59
60fn tokenize(src: &str) -> Result<Vec<Tok>> {
61    let b = src.as_bytes();
62    let mut i = 0;
63    let mut out = Vec::new();
64    while i < b.len() {
65        let c = b[i];
66        match c {
67            b' ' | b'\t' | b'\r' | b'\n' | b',' => i += 1, // whitespace + optional commas
68            b'/' if i + 1 < b.len() && b[i + 1] == b'/' => {
69                while i < b.len() && b[i] != b'\n' {
70                    i += 1;
71                }
72            }
73            b'{' => {
74                out.push(Tok::LBrace);
75                i += 1;
76            }
77            b'}' => {
78                out.push(Tok::RBrace);
79                i += 1;
80            }
81            b'<' => {
82                out.push(Tok::Lt);
83                i += 1;
84            }
85            b'>' => {
86                out.push(Tok::Gt);
87                i += 1;
88            }
89            b':' => {
90                out.push(Tok::Colon);
91                i += 1;
92            }
93            b'=' => {
94                out.push(Tok::Eq);
95                i += 1;
96            }
97            b'-' => {
98                out.push(Tok::Minus);
99                i += 1;
100            }
101            c if c.is_ascii_digit() => {
102                let start = i;
103                while i < b.len() && b[i].is_ascii_digit() {
104                    i += 1;
105                }
106                let n: u64 = src[start..i]
107                    .parse()
108                    .map_err(|_| err(format!("number too large: {}", &src[start..i])))?;
109                out.push(Tok::Int(n));
110            }
111            c if c.is_ascii_alphabetic() || c == b'_' => {
112                let start = i;
113                while i < b.len() && (b[i].is_ascii_alphanumeric() || b[i] == b'_') {
114                    i += 1;
115                }
116                out.push(Tok::Ident(src[start..i].to_string()));
117            }
118            other => return Err(err(format!("unexpected character {:?}", other as char))),
119        }
120    }
121    Ok(out)
122}
123
124// ---------------------------------------------------------------------------
125// Parser (owned AST so field-name refs outlive the SchemaBuilder calls)
126// ---------------------------------------------------------------------------
127
128enum Mode {
129    Sparse,
130    Dense,
131    Packed,
132}
133
134struct StructAst {
135    name: String,
136    mode: Mode,
137    fields: Vec<(u16, String, Dt)>,
138    defaults: Vec<(u16, Value)>,
139}
140struct EnumAst {
141    name: String,
142    variants: Vec<(u32, String)>,
143}
144
145struct Parser<'t> {
146    toks: &'t [Tok],
147    pos: usize,
148}
149
150impl<'t> Parser<'t> {
151    fn peek(&self) -> Option<&'t Tok> {
152        self.toks.get(self.pos)
153    }
154    fn next(&mut self) -> Result<&'t Tok> {
155        let t = self
156            .toks
157            .get(self.pos)
158            .ok_or_else(|| err("unexpected end of input"))?;
159        self.pos += 1;
160        Ok(t)
161    }
162    fn ident(&mut self) -> Result<String> {
163        match self.next()? {
164            Tok::Ident(s) => Ok(s.clone()),
165            other => Err(err(format!("expected a name, found {other:?}"))),
166        }
167    }
168    fn expect(&mut self, want: &Tok) -> Result<()> {
169        let got = self.next()?;
170        if got == want {
171            Ok(())
172        } else {
173            Err(err(format!("expected {want:?}, found {got:?}")))
174        }
175    }
176
177    /// A type expression: `list<T>`, a scalar keyword, or a named ref.
178    fn type_expr(&mut self) -> Result<Dt> {
179        let name = self.ident()?;
180        if name == "list" {
181            self.expect(&Tok::Lt)?;
182            let elem = self.type_expr()?;
183            self.expect(&Tok::Gt)?;
184            return Ok(Dt::list(elem));
185        }
186        if name == "map" {
187            // `map<K, V>` — the comma is optional (commas are whitespace).
188            self.expect(&Tok::Lt)?;
189            let key = self.type_expr()?;
190            let value = self.type_expr()?;
191            self.expect(&Tok::Gt)?;
192            return Ok(Dt::map(key, value));
193        }
194        if name == "union" {
195            // `union<T0, T1, …>` — one or more variant types (commas optional).
196            self.expect(&Tok::Lt)?;
197            let mut variants = Vec::new();
198            while self.peek() != Some(&Tok::Gt) {
199                variants.push(self.type_expr()?);
200            }
201            self.expect(&Tok::Gt)?;
202            return Ok(Dt::union(variants));
203        }
204        Ok(match name.as_str() {
205            "bool" => Dt::Bool,
206            "u8" => Dt::U8,
207            "u16" => Dt::U16,
208            "u32" => Dt::U32,
209            "u64" => Dt::U64,
210            "i8" => Dt::I8,
211            "i16" => Dt::I16,
212            "i32" => Dt::I32,
213            "i64" => Dt::I64,
214            "f32" => Dt::F32,
215            "f64" => Dt::F64,
216            "string" | "str" => Dt::Str,
217            "bytes" => Dt::Bytes,
218            // Anything else is a reference to a declared struct/enum.
219            _ => Dt::named(&name),
220        })
221    }
222
223    fn struct_def(&mut self, mode: Mode) -> Result<StructAst> {
224        let name = self.ident()?;
225        self.expect(&Tok::LBrace)?;
226        let mut fields = Vec::new();
227        let mut defaults = Vec::new();
228        while self.peek() != Some(&Tok::RBrace) {
229            let id = match self.next()? {
230                Tok::Int(n) if *n <= u16::MAX as u64 => *n as u16,
231                Tok::Int(n) => return Err(err(format!("field id {n} exceeds u16 in {name}"))),
232                other => {
233                    return Err(err(format!(
234                        "expected a field id in {name}, found {other:?}"
235                    )))
236                }
237            };
238            self.expect(&Tok::Colon)?;
239            let fname = self.ident()?;
240            let ty = self.type_expr()?;
241            // Optional `= <literal>` custom default (scalar fields only).
242            if self.peek() == Some(&Tok::Eq) {
243                self.next()?;
244                defaults.push((id, self.default_literal(&ty)?));
245            }
246            fields.push((id, fname, ty));
247        }
248        self.expect(&Tok::RBrace)?;
249        Ok(StructAst {
250            name,
251            mode,
252            fields,
253            defaults,
254        })
255    }
256
257    /// A scalar default literal (`= 5`, `= -3`, `= true`), typed by the field's
258    /// declared type `ty`.
259    fn default_literal(&mut self, ty: &Dt) -> Result<Value> {
260        if let Some(Tok::Ident(s)) = self.peek() {
261            if s == "true" || s == "false" {
262                let b = s == "true";
263                self.next()?;
264                return Ok(Value::Bool(b));
265            }
266        }
267        let neg = if self.peek() == Some(&Tok::Minus) {
268            self.next()?;
269            true
270        } else {
271            false
272        };
273        let n = match self.next()? {
274            Tok::Int(n) => *n,
275            other => return Err(err(format!("expected a default literal, found {other:?}"))),
276        };
277        let s = |n: u64| -> i64 {
278            if neg {
279                -(n as i64)
280            } else {
281                n as i64
282            }
283        };
284        Ok(match ty {
285            Dt::U8 => Value::U8(n as u8),
286            Dt::U16 => Value::U16(n as u16),
287            Dt::U32 => Value::U32(n as u32),
288            Dt::U64 => Value::U64(n),
289            Dt::I8 => Value::I8(s(n) as i8),
290            Dt::I16 => Value::I16(s(n) as i16),
291            Dt::I32 => Value::I32(s(n) as i32),
292            Dt::I64 => Value::I64(s(n)),
293            Dt::F32 => Value::F32(if neg { -(n as f32) } else { n as f32 }),
294            Dt::F64 => Value::F64(if neg { -(n as f64) } else { n as f64 }),
295            // A named type here is validated as an enum at build time.
296            Dt::Named(_) => Value::Enum(n as u32),
297            _ => return Err(err("a default is only allowed on a scalar field")),
298        })
299    }
300
301    fn enum_def(&mut self) -> Result<EnumAst> {
302        let name = self.ident()?;
303        self.expect(&Tok::LBrace)?;
304        let mut variants = Vec::new();
305        while self.peek() != Some(&Tok::RBrace) {
306            let value = match self.next()? {
307                Tok::Int(n) if *n <= u32::MAX as u64 => *n as u32,
308                Tok::Int(n) => return Err(err(format!("enum value {n} exceeds u32 in {name}"))),
309                other => {
310                    return Err(err(format!(
311                        "expected an enum value in {name}, found {other:?}"
312                    )))
313                }
314            };
315            self.expect(&Tok::Colon)?;
316            let vname = self.ident()?;
317            variants.push((value, vname));
318        }
319        self.expect(&Tok::RBrace)?;
320        Ok(EnumAst { name, variants })
321    }
322}
323
324/// Parse `.vsc` IDL source into a [`Schema`]. Errors are `BadSchema` with an
325/// `idl:` prefix and a human-readable message.
326pub fn parse(src: &str) -> Result<Schema> {
327    let toks = tokenize(src)?;
328    let mut p = Parser {
329        toks: &toks,
330        pos: 0,
331    };
332
333    let mut structs: Vec<StructAst> = Vec::new();
334    let mut enums: Vec<EnumAst> = Vec::new();
335    let mut root: Option<String> = None;
336
337    while let Some(t) = p.peek() {
338        let kw = match t {
339            Tok::Ident(s) => s.clone(),
340            other => {
341                return Err(err(format!(
342                    "expected a top-level declaration, found {other:?}"
343                )))
344            }
345        };
346        p.pos += 1; // consume the keyword
347        match kw.as_str() {
348            "struct" => structs.push(p.struct_def(Mode::Sparse)?),
349            "dense" => {
350                if p.ident()? != "struct" {
351                    return Err(err("`dense` must be followed by `struct`"));
352                }
353                structs.push(p.struct_def(Mode::Dense)?);
354            }
355            "packed" => {
356                if p.ident()? != "struct" {
357                    return Err(err("`packed` must be followed by `struct`"));
358                }
359                structs.push(p.struct_def(Mode::Packed)?);
360            }
361            "enum" => enums.push(p.enum_def()?),
362            "root" => {
363                if root.is_some() {
364                    return Err(err("more than one `root` declaration"));
365                }
366                root = Some(p.ident()?);
367            }
368            other => return Err(err(format!("unknown top-level keyword `{other}`"))),
369        }
370    }
371
372    let root = root.ok_or_else(|| err("missing `root <TypeName>` declaration"))?;
373
374    // Build the schema. Field-name refs borrow the AST, which outlives this.
375    let mut b = SchemaBuilder::new();
376    for s in &structs {
377        let fields: Vec<(u16, &str, Dt)> = s
378            .fields
379            .iter()
380            .map(|(id, n, ty)| (*id, n.as_str(), ty.clone()))
381            .collect();
382        b = match s.mode {
383            Mode::Sparse => b.add_struct(&s.name, fields),
384            Mode::Dense => b.add_dense_struct(&s.name, fields),
385            Mode::Packed => b.add_packed_struct(&s.name, fields),
386        };
387    }
388    for e in &enums {
389        let variants: Vec<(u32, &str)> = e.variants.iter().map(|(v, n)| (*v, n.as_str())).collect();
390        b = b.add_enum(&e.name, variants);
391    }
392    for s in &structs {
393        for (fid, value) in &s.defaults {
394            b = b.set_default(&s.name, *fid, value.clone());
395        }
396    }
397    b.build(&root)
398}
399
400#[cfg(test)]
401mod tests {
402    use super::*;
403
404    #[test]
405    fn idl_matches_hand_built_schema_id() {
406        let src = r#"
407            // an order record
408            dense struct Point { 1: x f64  2: y f64 }
409            enum Level { 0: Debug  1: Info  2: Error }
410            struct Order {
411                1: id     u64
412                2: item   string
413                3: qty    u32
414                4: tags   list<string>
415                5: origin Point
416                6: level  Level
417                7: grid   list<list<u16>>
418            }
419            root Order
420        "#;
421        let from_idl = parse(src).unwrap();
422
423        let hand = SchemaBuilder::new()
424            .add_dense_struct("Point", vec![(1, "x", Dt::F64), (2, "y", Dt::F64)])
425            .add_enum("Level", vec![(0, "Debug"), (1, "Info"), (2, "Error")])
426            .add_struct(
427                "Order",
428                vec![
429                    (1, "id", Dt::U64),
430                    (2, "item", Dt::Str),
431                    (3, "qty", Dt::U32),
432                    (4, "tags", Dt::list(Dt::Str)),
433                    (5, "origin", Dt::named("Point")),
434                    (6, "level", Dt::named("Level")),
435                    (7, "grid", Dt::list(Dt::list(Dt::U16))),
436                ],
437            )
438            .build("Order")
439            .unwrap();
440
441        // The IDL is a front-end to the canonical schema: same id, byte-for-byte.
442        assert_eq!(from_idl.id(), hand.id());
443        assert_eq!(from_idl.canonical_bytes(), hand.canonical_bytes());
444    }
445
446    #[test]
447    fn declaration_order_is_irrelevant() {
448        let a = parse("struct A { 1: x u8 } root A").unwrap();
449        let b = parse("  root A\nstruct A {1:x u8}").unwrap();
450        assert_eq!(a.id(), b.id());
451    }
452
453    #[test]
454    fn errors_are_typed_not_panics() {
455        assert!(parse("struct {").is_err()); // no name
456        assert!(parse("struct A { 1 x u8 } root A").is_err()); // missing colon
457        assert!(parse("struct A { 1: x u8 }").is_err()); // no root
458        assert!(parse("root Missing").is_err()); // root refers to nothing
459        assert!(parse("struct A { 99999999: x u8 } root A").is_err()); // field id > u16
460    }
461}