Skip to main content

limbo_macros/
lib.rs

1// UPSTREAM: vendored Limbo fork — allow upstream style
2//! Procedural macros backing the `oxisqlite-ext` extension API, part of the
3//! C-free **oxisqlite** engine (a Pure-Rust fork of limbo 0.0.22).
4//!
5//! Provides the `scalar` attribute macro, the `AggregateDerive`,
6//! `VTabModuleDerive`, and `VfsDerive` derive macros, and the
7//! `register_extension!` macro that wires them into the engine.
8#![allow(
9    rustdoc::bare_urls,
10    rustdoc::invalid_html_tags,
11    rustdoc::invalid_rust_codeblocks
12)]
13#![allow(clippy::collapsible_match)]
14
15mod ext;
16extern crate proc_macro;
17use proc_macro::{token_stream::IntoIter, Group, TokenStream, TokenTree};
18use std::collections::HashMap;
19
20/// A procedural macro that derives a `Description` trait for enums.
21/// This macro extracts documentation comments (specified with `/// Description...`) for enum variants
22/// and generates an implementation for `get_description`, which returns the associated description.
23#[proc_macro_derive(Description, attributes(desc))]
24pub fn derive_description_from_doc(item: TokenStream) -> TokenStream {
25    // Convert the TokenStream into an iterator of TokenTree
26    let mut tokens = item.into_iter();
27
28    let mut enum_name = String::new();
29
30    // Vector to store enum variants and their associated payloads (if any)
31    let mut enum_variants: Vec<(String, Option<String>)> = Vec::<(String, Option<String>)>::new();
32
33    // HashMap to store descriptions associated with each enum variant
34    let mut variant_description_map: HashMap<String, String> = HashMap::new();
35
36    // Parses the token stream to extract the enum name and its variants
37    while let Some(token) = tokens.next() {
38        match token {
39            TokenTree::Ident(ident) if ident.to_string() == "enum" => {
40                // Get the enum name
41                if let Some(TokenTree::Ident(name)) = tokens.next() {
42                    enum_name = name.to_string();
43                }
44            }
45            TokenTree::Group(group) => {
46                let mut group_tokens_iter: IntoIter = group.stream().into_iter();
47
48                let mut last_seen_desc: Option<String> = None;
49                while let Some(token) = group_tokens_iter.next() {
50                    match token {
51                        TokenTree::Punct(punct) => {
52                            if punct.to_string() == "#" {
53                                last_seen_desc = process_description(&mut group_tokens_iter);
54                            }
55                        }
56                        TokenTree::Ident(ident) => {
57                            // Capture the enum variant name and associate it with its description
58                            let ident_str = ident.to_string();
59                            if let Some(desc) = &last_seen_desc {
60                                variant_description_map.insert(ident_str.clone(), desc.clone());
61                            }
62                            enum_variants.push((ident_str, None));
63                            last_seen_desc = None;
64                        }
65                        TokenTree::Group(group) => {
66                            // Capture payload information for the current enum variant
67                            if let Some(last_variant) = enum_variants.last_mut() {
68                                last_variant.1 = Some(process_payload(group));
69                            }
70                        }
71                        _ => {}
72                    }
73                }
74            }
75            _ => {}
76        }
77    }
78    generate_get_description(enum_name, &variant_description_map, enum_variants)
79}
80
81/// Processes a Rust docs to extract the description string.
82fn process_description(token_iter: &mut IntoIter) -> Option<String> {
83    if let Some(TokenTree::Group(doc_group)) = token_iter.next() {
84        let mut doc_group_iter = doc_group.stream().into_iter();
85        // Skip the `desc` and `(` tokens to reach the actual description
86        doc_group_iter.next();
87        doc_group_iter.next();
88        if let Some(TokenTree::Literal(description)) = doc_group_iter.next() {
89            return Some(description.to_string());
90        }
91    }
92    None
93}
94
95/// Processes the payload of an enum variant to extract variable names (ignoring types).
96fn process_payload(payload_group: Group) -> String {
97    let payload_group_iter = payload_group.stream().into_iter();
98    let mut variable_name_list = String::from("");
99    let mut is_variable_name = true;
100    for token in payload_group_iter {
101        match token {
102            TokenTree::Ident(ident) => {
103                if is_variable_name {
104                    variable_name_list.push_str(&format!("{},", ident));
105                }
106                is_variable_name = false;
107            }
108            TokenTree::Punct(punct) => {
109                if punct.to_string() == "," {
110                    is_variable_name = true;
111                }
112            }
113            _ => {}
114        }
115    }
116    format!("{{ {} }}", variable_name_list).to_string()
117}
118/// Generates the `get_description` implementation for the processed enum.
119fn generate_get_description(
120    enum_name: String,
121    variant_description_map: &HashMap<String, String>,
122    enum_variants: Vec<(String, Option<String>)>,
123) -> TokenStream {
124    let mut all_enum_arms = String::from("");
125    for (variant, payload) in enum_variants {
126        let payload = payload.unwrap_or("".to_string());
127        let desc;
128        if let Some(description) = variant_description_map.get(&variant) {
129            desc = format!("Some({})", description);
130        } else {
131            desc = "None".to_string();
132        }
133        all_enum_arms.push_str(&format!(
134            "{}::{} {} => {},\n",
135            enum_name, variant, payload, desc
136        ));
137    }
138
139    let enum_impl = format!(
140        "impl {}  {{ 
141     pub fn get_description(&self) -> Option<&str> {{
142     match self {{
143     {}
144     }}
145     }}
146     }}",
147        enum_name, all_enum_arms
148    );
149    enum_impl
150        .parse()
151        .expect("generated enum impl should be valid Rust token stream")
152}
153
154/// Register your extension with 'core' by providing the relevant functions
155///```ignore
156///use limbo_ext::{register_extension, scalar, Value, AggregateDerive, AggFunc};
157///
158/// register_extension!{ scalars: { return_one }, aggregates: { SumPlusOne } }
159///
160///#[scalar(name = "one")]
161///fn return_one(args: &[Value]) -> Value {
162///  return Value::from_integer(1);
163///}
164///
165///#[derive(AggregateDerive)]
166///struct SumPlusOne;
167///
168///impl AggFunc for SumPlusOne {
169///   type State = i64;
170///   const NAME: &'static str = "sum_plus_one";
171///   const ARGS: i32 = 1;
172///
173///   fn step(state: &mut Self::State, args: &[Value]) {
174///      let Some(val) = args[0].to_integer() else {
175///        return;
176///      };
177///      *state += val;
178///     }
179///
180///     fn finalize(state: Self::State) -> Value {
181///        Value::from_integer(state + 1)
182///     }
183///}
184///
185/// ```
186#[proc_macro]
187pub fn register_extension(input: TokenStream) -> TokenStream {
188    ext::register_extension(input)
189}
190
191/// Declare a scalar function for your extension. This requires the name:
192/// #[scalar(name = "example")] of what you wish to call your function with.
193/// ```text
194/// use limbo_ext::{scalar, Value};
195/// #[scalar(name = "double", alias = "twice")] // you can provide an <optional> alias
196/// fn double(args: &[Value]) -> Value {
197///       let arg = args.get(0).unwrap();
198///       match arg.value_type() {
199///           ValueType::Float => {
200///               let val = arg.to_float().unwrap();
201///               Value::from_float(val * 2.0)
202///           }
203///           ValueType::Integer => {
204///               let val = arg.to_integer().unwrap();
205///               Value::from_integer(val * 2)
206///           }
207///       }
208///   } else {
209///       Value::null()
210///   }
211/// }
212/// ```
213#[proc_macro_attribute]
214pub fn scalar(attr: TokenStream, input: TokenStream) -> TokenStream {
215    ext::scalar(attr, input)
216}
217
218/// Define an aggregate function for your extension by deriving
219/// AggregateDerive on a struct that implements the AggFunc trait.
220/// ```ignore
221/// use limbo_ext::{register_extension, Value, AggregateDerive, AggFunc};
222///
223///#[derive(AggregateDerive)]
224///struct SumPlusOne;
225///
226///impl AggFunc for SumPlusOne {
227///   type State = i64;
228///   type Error = &'static str;
229///   const NAME: &'static str = "sum_plus_one";
230///   const ARGS: i32 = 1;
231///   fn step(state: &mut Self::State, args: &[Value]) {
232///      let Some(val) = args[0].to_integer() else {
233///        return;
234///     };
235///     *state += val;
236///     }
237///     fn finalize(state: Self::State) -> Result<Value, Self::Error> {
238///        Ok(Value::from_integer(state + 1))
239///     }
240///}
241/// ```
242#[proc_macro_derive(AggregateDerive)]
243pub fn derive_agg_func(input: TokenStream) -> TokenStream {
244    ext::derive_agg_func(input)
245}
246
247/// Macro to derive a VTabModule for your extension. This macro will generate
248/// the necessary functions to register your module with core. You must implement
249/// the VTabModule, VTable, and VTabCursor traits.
250/// ```ignore
251/// #[derive(Debug, VTabModuleDerive)]
252/// struct CsvVTabModule;
253///
254/// impl VTabModule for CsvVTabModule {
255///  type Table = CsvTable;
256///  const NAME: &'static str = "csv_data";
257///  const VTAB_KIND: VTabKind = VTabKind::VirtualTable;
258///
259///   /// Declare your virtual table and its schema
260///  fn create(args: &[Value]) -> Result<(String, Self::Table), ResultCode> {
261///     let schema = "CREATE TABLE csv_data(
262///             name TEXT,
263///             age TEXT,
264///             city TEXT
265///         )".into();
266///     Ok((schema, CsvTable {}))
267///  }
268/// }
269///
270/// struct CsvTable {}
271///
272/// // Implement the VTable trait for your virtual table
273/// impl VTable for CsvTable {
274///  type Cursor = CsvCursor;
275///  type Error = &'static str;
276///
277///  /// Open the virtual table and return a cursor
278///  fn open(&self) -> Result<Self::Cursor, Self::Error> {
279///     let csv_content = fs::read_to_string("data.csv").unwrap_or_default();
280///     let rows: Vec<Vec<String>> = csv_content
281///         .lines()
282///         .skip(1)
283///         .map(|line| {
284///             line.split(',')
285///                 .map(|s| s.trim().to_string())
286///                 .collect()
287///         })
288///         .collect();
289///     Ok(CsvCursor { rows, index: 0 })
290///  }
291///
292/// /// **Optional** methods for non-readonly tables:
293///
294///  /// Update the row with the provided values, return the new rowid
295///  fn update(&mut self, rowid: i64, args: &[Value]) -> Result<Option<i64>, Self::Error> {
296///      Ok(None)// return Ok(None) for read-only
297///  }
298///
299///  /// Insert a new row with the provided values, return the new rowid
300///  fn insert(&mut self, args: &[Value]) -> Result<(), Self::Error> {
301///      Ok(()) //
302///  }
303///
304///  /// Delete the row with the provided rowid
305///  fn delete(&mut self, rowid: i64) -> Result<(), Self::Error> {
306///    Ok(())
307///  }
308///
309///  /// Destroy the virtual table. Any cleanup logic for when the table is deleted comes heres
310///  fn destroy(&mut self) -> Result<(), Self::Error> {
311///     Ok(())
312///  }
313/// }
314///
315///  #[derive(Debug)]
316/// struct CsvCursor {
317///   rows: Vec<Vec<String>>,
318///   index: usize,
319/// }
320///
321/// impl CsvCursor {
322///   /// Returns the value for a given column index.
323///   fn column(&self, idx: u32) -> Result<Value, Self::Error> {
324///       let row = &self.rows[self.index];
325///       if (idx as usize) < row.len() {
326///           Value::from_text(&row[idx as usize])
327///       } else {
328///           Value::null()
329///       }
330///   }
331/// }
332///
333/// // Implement the VTabCursor trait for your virtual cursor
334/// impl VTabCursor for CsvCursor {
335///  type Error = &'static str;
336///
337///  /// Filter the virtual table based on arguments (omitted here for simplicity)
338///  fn filter(&mut self, _args: &[Value], _idx_info: Option<(&str, i32)>) -> ResultCode {
339///      ResultCode::OK
340///  }
341///
342///  /// Move the cursor to the next row
343///  fn next(&mut self) -> ResultCode {
344///     if self.index < self.rows.len() - 1 {
345///         self.index += 1;
346///         ResultCode::OK
347///     } else {
348///         ResultCode::EOF
349///     }
350///  }
351///
352///  fn eof(&self) -> bool {
353///      self.index >= self.rows.len()
354///  }
355///
356///  /// Return the value for a given column index
357///  fn column(&self, idx: u32) -> Result<Value, Self::Error> {
358///      self.column(idx)
359///  }
360///
361///  fn rowid(&self) -> i64 {
362///      self.index as i64
363///  }
364/// }
365///
366#[proc_macro_derive(VTabModuleDerive)]
367pub fn derive_vtab_module(input: TokenStream) -> TokenStream {
368    ext::derive_vtab_module(input)
369}
370
371/// ```text
372/// use limbo_ext::{ExtResult as Result, VfsDerive, VfsExtension, VfsFile};
373///
374/// // Your struct must also impl Default
375/// #[derive(VfsDerive, Default)]
376/// struct ExampleFS;
377///
378///
379/// struct ExampleFile {
380///    file: std::fs::File,
381///
382///
383/// impl VfsExtension for ExampleFS {
384///    /// The name of your vfs module
385///    const NAME: &'static str = "example";
386///
387///    type File = ExampleFile;
388///
389///    fn open(&self, path: &str, flags: i32, _direct: bool) -> Result<Self::File> {
390///        let file = OpenOptions::new()
391///            .read(true)
392///            .write(true)
393///            .create(flags & 1 != 0)
394///            .open(path)
395///            .map_err(|_| ResultCode::Error)?;
396///        Ok(TestFile { file })
397///    }
398///
399///    fn run_once(&self) -> Result<()> {
400///    // (optional) method to cycle/advance IO, if your extension is asynchronous
401///        Ok(())
402///    }
403///
404///    fn close(&self, file: Self::File) -> Result<()> {
405///    // (optional) method to close or drop the file
406///        Ok(())
407///    }
408///
409///    fn generate_random_number(&self) -> i64 {
410///    // (optional) method to generate random number. Used for testing
411///        let mut buf = [0u8; 8];
412///        getrandom::fill(&mut buf).unwrap();
413///        i64::from_ne_bytes(buf)
414///    }
415///
416///   fn get_current_time(&self) -> String {
417///    // (optional) method to generate random number. Used for testing
418///        chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string()
419///    }
420///
421///
422/// impl VfsFile for ExampleFile {
423///    fn read(
424///        &mut self,
425///        buf: &mut [u8],
426///        count: usize,
427///        offset: i64,
428///    ) -> Result<i32> {
429///        if file.file.seek(SeekFrom::Start(offset as u64)).is_err() {
430///            return Err(ResultCode::Error);
431///        }
432///        file.file
433///            .read(&mut buf[..count])
434///            .map_err(|_| ResultCode::Error)
435///            .map(|n| n as i32)
436///    }
437///
438///    fn write(&mut self, buf: &[u8], count: usize, offset: i64) -> Result<i32> {
439///        if self.file.seek(SeekFrom::Start(offset as u64)).is_err() {
440///            return Err(ResultCode::Error);
441///        }
442///        self.file
443///            .write(&buf[..count])
444///            .map_err(|_| ResultCode::Error)
445///            .map(|n| n as i32)
446///    }
447///
448///    fn sync(&self) -> Result<()> {
449///        self.file.sync_all().map_err(|_| ResultCode::Error)
450///    }
451///
452///    fn size(&self) -> i64 {
453///      self.file.metadata().map(|m| m.len() as i64).unwrap_or(-1)
454///   }
455///}
456///
457///```
458#[proc_macro_derive(VfsDerive)]
459pub fn derive_vfs_module(input: TokenStream) -> TokenStream {
460    ext::derive_vfs_module(input)
461}