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}