diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab6104b7..90b4f95b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -173,7 +173,7 @@ jobs: - name: cargo doc env: - RUSTDOCFLAGS: -D warnings + RUSTDOCFLAGS: -D warnings --cfg docsrs run: cargo doc --workspace --all-features --no-deps --document-private-items rust-tests: diff --git a/trustfall_core/Cargo.toml b/trustfall_core/Cargo.toml index 61532b1b..94a221d3 100644 --- a/trustfall_core/Cargo.toml +++ b/trustfall_core/Cargo.toml @@ -8,6 +8,9 @@ description = "The trustfall query engine, empowering you to query everything." repository = "https://github.com/obi1kenobi/trustfall" readme = "../README.md" +[package.metadata.docs.rs] +rustdoc-args = ["--cfg", "docsrs"] + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib] diff --git a/trustfall_core/src/interpreter/execution.rs b/trustfall_core/src/interpreter/execution.rs index 84abc190..5636056e 100644 --- a/trustfall_core/src/interpreter/execution.rs +++ b/trustfall_core/src/interpreter/execution.rs @@ -2,6 +2,7 @@ use std::{ cell::RefCell, collections::{BTreeMap, BTreeSet}, fmt::Debug, + ops::DerefMut, rc::Rc, sync::Arc, }; @@ -40,12 +41,13 @@ where Vertex: Clone + Debug + 'query, { let query = InterpretedQuery::from_query_and_arguments(indexed_query, arguments)?; - let ir_query = &query.indexed_query.ir_query; + let root_vid = query.indexed_query.ir_query.root_component.root; + let ir_query = &query.indexed_query.ir_query; let root_edge = &ir_query.root_name; let root_edge_parameters = &ir_query.root_parameters; + let mut query_info = QueryInfo::new(query.clone(), root_vid, None); - let query_info = QueryInfo::new(query.clone(), ir_query.root_component.root, None); let mut adapter_ref = adapter.borrow_mut(); let mut iterator: ContextIterator<'query, Vertex> = Box::new( adapter_ref @@ -55,14 +57,18 @@ where drop(adapter_ref); let component = &ir_query.root_component; - iterator = compute_component(adapter.clone(), &query, component, iterator); + iterator = compute_component(adapter.clone(), &mut query_info, component, iterator); - Ok(construct_outputs(adapter.as_ref(), &query, iterator)) + Ok(construct_outputs( + adapter.as_ref(), + &mut query_info, + iterator, + )) } fn coerce_if_needed<'query, Vertex>( adapter: &RefCell + 'query>, - query: &InterpretedQuery, + query: &mut QueryInfo, vertex: &IRVertex, iterator: ContextIterator<'query, Vertex>, ) -> ContextIterator<'query, Vertex> @@ -84,7 +90,7 @@ where fn perform_coercion<'query, Vertex>( adapter: &RefCell + 'query>, - query: &InterpretedQuery, + query: &mut QueryInfo, vertex: &IRVertex, coerced_from: &Arc, coerce_to: &Arc, @@ -93,10 +99,10 @@ fn perform_coercion<'query, Vertex>( where Vertex: Clone + Debug + 'query, { - let query_info = QueryInfo::new(query.clone(), vertex.vid, None); let mut adapter_ref = adapter.borrow_mut(); - let coercion_iter = - adapter_ref.resolve_coercion(iterator, coerced_from, coerce_to, &query_info); + query.current_vertex = vertex.vid; + query.crossing_eid = None; + let coercion_iter = adapter_ref.resolve_coercion(iterator, coerced_from, coerce_to, query); Box::new(coercion_iter.filter_map( |(ctx, can_coerce)| { @@ -111,7 +117,7 @@ where fn compute_component<'query, Vertex>( adapter: Rc + 'query>>, - query: &InterpretedQuery, + query: &mut QueryInfo, component: &IRQueryComponent, mut iterator: ContextIterator<'query, Vertex>, ) -> ContextIterator<'query, Vertex> @@ -199,31 +205,40 @@ where fn construct_outputs<'query, Vertex: Clone + Debug + 'query>( adapter: &RefCell>, - query: &InterpretedQuery, + query: &mut QueryInfo, iterator: ContextIterator<'query, Vertex>, ) -> Box, FieldValue>> + 'query> { - let ir_query = &query.indexed_query.ir_query; - let mut output_names: Vec> = ir_query.root_component.outputs.keys().cloned().collect(); + let mut output_names: Vec> = query + .ir_query() + .root_component + .outputs + .keys() + .cloned() + .collect(); output_names.sort_unstable(); // to ensure deterministic resolve_property() ordering let mut output_iterator = iterator; for output_name in output_names.iter() { - let context_field = &ir_query.root_component.outputs[output_name]; + let component = query.ir_query().root_component.clone(); + let context_field = &component.outputs[output_name]; let vertex_id = context_field.vertex_id; + let moved_iterator = Box::new(output_iterator.map(move |context| { let new_vertex = context.vertices[&vertex_id].clone(); context.move_to_vertex(new_vertex) })); - let type_name = &ir_query.root_component.vertices[&vertex_id].type_name; let mut adapter_ref = adapter.borrow_mut(); - let query_info = QueryInfo::new(query.clone(), vertex_id, None); + query.current_vertex = vertex_id; + query.crossing_eid = None; + + let type_name = &query.ir_query().root_component.vertices[&vertex_id].type_name; let field_data_iterator = adapter_ref.resolve_property( moved_iterator, type_name, &context_field.field_name, - &query_info, + query, ); drop(adapter_ref); @@ -233,7 +248,7 @@ fn construct_outputs<'query, Vertex: Clone + Debug + 'query>( })); } - let expected_output_names: BTreeSet<_> = query.indexed_query.outputs.keys().cloned().collect(); + let expected_output_names: BTreeSet<_> = query.outputs().keys().cloned().collect(); Box::new(output_iterator.map(move |mut context| { assert!(context.values.len() == output_names.len()); @@ -258,7 +273,7 @@ fn construct_outputs<'query, Vertex: Clone + Debug + 'query>( /// If this IRFold has a filter on the folded element count, and that filter imposes /// a max size that can be statically determined, return that max size so it can /// be used for further optimizations. Otherwise, return None. -fn get_max_fold_count_limit(query: &InterpretedQuery, fold: &IRFold) -> Option { +fn get_max_fold_count_limit(query: &mut QueryInfo, fold: &IRFold) -> Option { let mut result: Option = None; for post_fold_filter in fold.post_filters.iter() { @@ -268,11 +283,15 @@ fn get_max_fold_count_limit(query: &InterpretedQuery, fold: &IRFold) -> Option { - let variable_value = query.arguments[&var_ref.variable_name].as_usize().unwrap(); + let variable_value = query.arguments()[&var_ref.variable_name] + .as_usize() + .unwrap(); Some(variable_value) } Operation::LessThan(FoldSpecificFieldKind::Count, Argument::Variable(var_ref)) => { - let variable_value = query.arguments[&var_ref.variable_name].as_usize().unwrap(); + let variable_value = query.arguments()[&var_ref.variable_name] + .as_usize() + .unwrap(); // saturating_sub() here is a safeguard against underflow: in principle, // we shouldn't see a comparison for "< 0", but if we do regardless, we'd prefer to // saturate to 0 rather than wrapping around. This check is an optimization and @@ -281,7 +300,7 @@ fn get_max_fold_count_limit(query: &InterpretedQuery, fold: &IRFold) -> Option { - match &query.arguments[&var_ref.variable_name] { + match &query.arguments()[&var_ref.variable_name] { FieldValue::List(v) => v.iter().map(|x| x.as_usize().unwrap()).max(), _ => unreachable!(), } @@ -340,7 +359,7 @@ fn collect_fold_elements<'query, Vertex: Clone + Debug + 'query>( #[allow(unused_variables)] fn compute_fold<'query, Vertex: Clone + Debug + 'query>( adapter: Rc + 'query>>, - query: &InterpretedQuery, + query: &mut QueryInfo, expanding_from: &IRVertex, parent_component: &IRQueryComponent, fold: Arc, @@ -358,12 +377,14 @@ fn compute_fold<'query, Vertex: Clone + Debug + 'query>( let field_vertex = &parent_component.vertices[&field.vertex_id]; let type_name = &field_vertex.type_name; - let query_info = QueryInfo::new(query.clone(), field.vertex_id, None); + query.current_vertex = vertex_id; + query.crossing_eid = None; + let context_and_value_iterator = adapter_ref.resolve_property( activated_vertex_iterator, type_name, &field.field_name, - &query_info, + query, ); let cloned_field = imported_field.clone(); @@ -399,20 +420,22 @@ fn compute_fold<'query, Vertex: Clone + Debug + 'query>( let activated_vertex_iterator: ContextIterator<'query, Vertex> = Box::new(iterator.map(move |x| x.activate_vertex(&expanding_from_vid))); let type_name = &expanding_from.type_name; - let query_info = QueryInfo::new(query.clone(), expanding_from_vid, Some(fold.eid)); + query.current_vertex = expanding_from_vid; + query.crossing_eid = Some(fold.eid); + let edge_iterator = adapter_ref.resolve_neighbors( activated_vertex_iterator, type_name, &fold.edge_name, &fold.parameters, - &query_info, + query, ); drop(adapter_ref); // Materialize the full fold data. // These values are moved into the closure. let cloned_adapter = adapter.clone(); - let cloned_query = query.clone(); + let mut cloned_query = query.clone(); let fold_component = fold.component.clone(); let fold_eid = fold.eid; let max_fold_size = get_max_fold_count_limit(query, fold.as_ref()); @@ -428,7 +451,7 @@ fn compute_fold<'query, Vertex: Clone + Debug + 'query>( let computed_iterator = compute_component( cloned_adapter.clone(), - &cloned_query, + &mut cloned_query, &fold_component, neighbor_contexts, ); @@ -474,7 +497,7 @@ fn compute_fold<'query, Vertex: Clone + Debug + 'query>( output_names.sort_unstable(); // to ensure deterministic resolve_property() ordering let cloned_adapter = adapter.clone(); - let cloned_query = query.clone(); + let mut cloned_query = query.clone(); let fold_component = fold.component.clone(); let final_iterator = post_filtered_iterator.map(move |mut ctx| { // If the @fold is inside an @optional that doesn't exist, @@ -539,12 +562,13 @@ fn compute_fold<'query, Vertex: Clone + Debug + 'query>( })); let mut adapter_ref = cloned_adapter.borrow_mut(); - let query_info = QueryInfo::new(cloned_query.clone(), vertex_id, None); + cloned_query.current_vertex = vertex_id; + cloned_query.crossing_eid = None; let field_data_iterator = adapter_ref.resolve_property( moved_iterator, &fold.component.vertices[&vertex_id].type_name, &context_field.field_name, - &query_info, + &cloned_query, ); drop(adapter_ref); @@ -665,7 +689,7 @@ macro_rules! implement_negated_filter { fn apply_local_field_filter<'query, Vertex: Clone + Debug + 'query>( adapter_ref: &RefCell + 'query>, - query: &InterpretedQuery, + query: &mut QueryInfo, component: &IRQueryComponent, current_vid: Vid, filter: &Operation, @@ -693,7 +717,7 @@ fn apply_local_field_filter<'query, Vertex: Clone + Debug + 'query>( fn apply_fold_specific_filter<'query, Vertex: Clone + Debug + 'query>( adapter_ref: &RefCell + 'query>, - query: &InterpretedQuery, + query: &mut QueryInfo, component: &IRQueryComponent, fold: &IRFold, current_vid: Vid, @@ -715,7 +739,7 @@ fn apply_fold_specific_filter<'query, Vertex: Clone + Debug + 'query>( fn apply_filter<'query, Vertex: Clone + Debug + 'query, LeftT: Debug + Clone + PartialEq + Eq>( adapter_ref: &RefCell + 'query>, - query: &InterpretedQuery, + query: &mut QueryInfo, component: &IRQueryComponent, current_vid: Vid, filter: &Operation, @@ -760,7 +784,7 @@ fn apply_filter<'query, Vertex: Clone + Debug + 'query, LeftT: Debug + Clone + P } } Some(Argument::Variable(var)) => { - let right_value = query.arguments[var.variable_name.as_ref()].to_owned(); + let right_value = query.arguments()[var.variable_name.as_ref()].to_owned(); Box::new(iterator.map(move |mut ctx| { // TODO: implement more efficient filtering with: // - no clone of runtime parameter values @@ -852,7 +876,7 @@ fn apply_filter<'query, Vertex: Clone + Debug + 'query, LeftT: Debug + Clone + P implement_filter!(expression_iterator, right, regex_matches_slow_path) } Argument::Variable(var) => { - let variable_value = &query.arguments[var.variable_name.as_ref()]; + let variable_value = &query.arguments()[var.variable_name.as_ref()]; let pattern = Regex::new(variable_value.as_str().unwrap()).unwrap(); Box::new(expression_iterator.filter_map(move |mut context| { @@ -872,7 +896,7 @@ fn apply_filter<'query, Vertex: Clone + Debug + 'query, LeftT: Debug + Clone + P implement_negated_filter!(expression_iterator, right, regex_matches_slow_path) } Argument::Variable(var) => { - let variable_value = &query.arguments[var.variable_name.as_ref()]; + let variable_value = &query.arguments()[var.variable_name.as_ref()]; let pattern = Regex::new(variable_value.as_str().unwrap()).unwrap(); Box::new(expression_iterator.filter_map(move |mut context| { @@ -890,13 +914,39 @@ fn apply_filter<'query, Vertex: Clone + Debug + 'query, LeftT: Debug + Clone + P } } -fn compute_context_field<'query, Vertex: Clone + Debug + 'query>( - adapter: &RefCell>, - query: &InterpretedQuery, +fn compute_context_field<'query, AdapterT: Adapter<'query>>( + adapter: &RefCell, + query: &mut QueryInfo, component: &IRQueryComponent, context_field: &ContextField, - iterator: ContextIterator<'query, Vertex>, -) -> ContextIterator<'query, Vertex> { + iterator: Box> + 'query>, +) -> Box> + 'query> { + let mut adapter_ref = adapter.borrow_mut(); + + let output_iterator = compute_context_field_with_separate_value( + adapter_ref.deref_mut(), + query, + component, + context_field, + iterator, + ) + .map(|(mut context, value)| { + context.values.push(value); + context + }); + + drop(adapter_ref); + + Box::new(output_iterator) +} + +pub(super) fn compute_context_field_with_separate_value<'query, AdapterT: Adapter<'query>>( + adapter: &mut AdapterT, + query: &mut QueryInfo, + component: &IRQueryComponent, + context_field: &ContextField, + iterator: Box> + 'query>, +) -> Box, FieldValue)> + 'query> { let vertex_id = context_field.vertex_id; if let Some(vertex) = component.vertices.get(&vertex_id) { @@ -908,33 +958,30 @@ fn compute_context_field<'query, Vertex: Clone + Debug + 'query>( }); let type_name = &vertex.type_name; - let mut adapter_ref = adapter.borrow_mut(); - let query_info = QueryInfo::new(query.clone(), vertex_id, None); - let context_and_value_iterator = adapter_ref.resolve_property( - Box::new(moved_iterator), - type_name, - &context_field.field_name, - &query_info, - ); - drop(adapter_ref); - - Box::new(context_and_value_iterator.map(|(mut context, value)| { - context.values.push(value); + query.current_vertex = vertex_id; + query.crossing_eid = None; + let context_and_value_iterator = adapter + .resolve_property( + Box::new(moved_iterator), + type_name, + &context_field.field_name, + query, + ) + .map(|(mut context, value)| { + // Make sure that the context has the same "current" token + // as before evaluating the context field. + let old_current_token = context.suspended_vertices.pop().unwrap(); + (context.move_to_vertex(old_current_token), value) + }); - // Make sure that the context has the same "current" vertex - // as before evaluating the context field. - let old_active_vertex = context.suspended_vertices.pop().unwrap(); - context.move_to_vertex(old_active_vertex) - })) + Box::new(context_and_value_iterator) } else { // This context field represents an imported tag value from an outer component. // Grab its value from the context itself. let field_ref = FieldRef::ContextField(context_field.clone()); - Box::new(iterator.map(move |mut context| { + Box::new(iterator.map(move |context| { let value = context.imported_tags[&field_ref].clone(); - context.values.push(value); - - context + (context, value) })) } } @@ -955,7 +1002,7 @@ fn compute_fold_specific_field<'query, Vertex: Clone + Debug + 'query>( fn compute_local_field<'query, Vertex: Clone + Debug + 'query>( adapter: &RefCell>, - query: &InterpretedQuery, + query: &mut QueryInfo, component: &IRQueryComponent, current_vid: Vid, local_field: &LocalField, @@ -963,9 +1010,10 @@ fn compute_local_field<'query, Vertex: Clone + Debug + 'query>( ) -> ContextIterator<'query, Vertex> { let type_name = &component.vertices[¤t_vid].type_name; let mut adapter_ref = adapter.borrow_mut(); - let query_info = QueryInfo::new(query.clone(), current_vid, None); + query.current_vertex = current_vid; + query.crossing_eid = None; let context_and_value_iterator = - adapter_ref.resolve_property(iterator, type_name, &local_field.field_name, &query_info); + adapter_ref.resolve_property(iterator, type_name, &local_field.field_name, query); drop(adapter_ref); Box::new(context_and_value_iterator.map(|(mut context, value)| { @@ -1044,7 +1092,7 @@ impl<'query, Vertex: Clone + Debug + 'query> Iterator for EdgeExpander<'query, V fn expand_edge<'query, Vertex: Clone + Debug + 'query>( adapter: Rc + 'query>>, - query: &InterpretedQuery, + query: &mut QueryInfo, component: &IRQueryComponent, expanding_from_vid: Vid, expanding_to_vid: Vid, @@ -1091,7 +1139,7 @@ fn expand_edge<'query, Vertex: Clone + Debug + 'query>( #[allow(clippy::too_many_arguments)] fn expand_non_recursive_edge<'query, Vertex: Clone + Debug + 'query>( adapter: Rc + 'query>>, - query: &InterpretedQuery, + query: &mut QueryInfo, _component: &IRQueryComponent, expanding_from: &IRVertex, _expanding_to: &IRVertex, @@ -1106,14 +1154,15 @@ fn expand_non_recursive_edge<'query, Vertex: Clone + Debug + 'query>( Box::new(iterator.map(move |x| x.activate_vertex(&expanding_from_vid))); let type_name = &expanding_from.type_name; - let query_info = QueryInfo::new(query.clone(), expanding_from_vid, Some(edge_id)); + query.current_vertex = expanding_from_vid; + query.crossing_eid = Some(edge_id); let mut adapter_ref = adapter.borrow_mut(); let edge_iterator = adapter_ref.resolve_neighbors( expanding_vertex_iterator, type_name, edge_name, edge_parameters, - &query_info, + query, ); drop(adapter_ref); @@ -1128,7 +1177,7 @@ fn expand_non_recursive_edge<'query, Vertex: Clone + Debug + 'query>( /// - record the vertex at this Vid in the context fn perform_entry_into_new_vertex<'query, Vertex: Clone + Debug + 'query>( adapter: Rc + 'query>>, - query: &InterpretedQuery, + query: &mut QueryInfo, component: &IRQueryComponent, vertex: &IRVertex, iterator: ContextIterator<'query, Vertex>, @@ -1154,7 +1203,7 @@ fn perform_entry_into_new_vertex<'query, Vertex: Clone + Debug + 'query>( #[allow(clippy::too_many_arguments)] fn expand_recursive_edge<'query, Vertex: Clone + Debug + 'query>( adapter: Rc + 'query>>, - query: &InterpretedQuery, + query: &mut QueryInfo, component: &IRQueryComponent, expanding_from: &IRVertex, expanding_to: &IRVertex, @@ -1197,13 +1246,14 @@ fn expand_recursive_edge<'query, Vertex: Clone + Debug + 'query>( for _ in 2..=max_depth { if let Some(coerce_to) = recursive.coerce_to.as_ref() { - let query_info = QueryInfo::new(query.clone(), expanding_from_vid, None); + query.current_vertex = expanding_from_vid; + query.crossing_eid = None; let mut adapter_ref = adapter.borrow_mut(); let coercion_iter = adapter_ref.resolve_coercion( recursion_iterator, edge_endpoint_type, coerce_to, - &query_info, + query, ); // This coercion is unusual since it doesn't discard elements that can't be coerced. @@ -1238,7 +1288,7 @@ fn expand_recursive_edge<'query, Vertex: Clone + Debug + 'query>( #[allow(clippy::too_many_arguments)] fn perform_one_recursive_edge_expansion<'query, Vertex: Clone + Debug + 'query>( adapter: Rc + 'query>>, - query: &InterpretedQuery, + query: &mut QueryInfo, _component: &IRQueryComponent, expanding_from_type: &Arc, expanding_from: &IRVertex, @@ -1248,14 +1298,15 @@ fn perform_one_recursive_edge_expansion<'query, Vertex: Clone + Debug + 'query>( edge_parameters: &EdgeParameters, iterator: ContextIterator<'query, Vertex>, ) -> ContextIterator<'query, Vertex> { - let query_info = QueryInfo::new(query.clone(), expanding_from.vid, Some(edge_id)); + query.current_vertex = expanding_from.vid; + query.crossing_eid = Some(edge_id); let mut adapter_ref = adapter.borrow_mut(); let edge_iterator = adapter_ref.resolve_neighbors( iterator, expanding_from_type, edge_name, edge_parameters, - &query_info, + query, ); drop(adapter_ref); diff --git a/trustfall_core/src/interpreter/hints/candidates.rs b/trustfall_core/src/interpreter/hints/candidates.rs new file mode 100644 index 00000000..a6464b99 --- /dev/null +++ b/trustfall_core/src/interpreter/hints/candidates.rs @@ -0,0 +1,233 @@ +use std::{ + fmt::Debug, + ops::{Bound, RangeBounds}, +}; + +use serde::{Deserialize, Serialize}; + +use crate::ir::FieldValue; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CandidateValue { + Impossible, // statically determined that no values fit + Single(T), // there's only one value that fits + Multiple(Vec), // there are multiple values that fit + All, // all values are possible for this specific entry + // (e.g. used for tag-from-non-existent-optional cases) +} + +impl CandidateValue { + pub(super) fn merge(&mut self, other: CandidateValue) { + match self { + Self::Impossible => {} // still impossible + Self::Single(val) => { + // It can only be single value, + // but might become impossible depending on the other side. + match other { + Self::Impossible => *self = CandidateValue::Impossible, + Self::Single(other) => { + if val != &other { + *self = CandidateValue::Impossible; + } + } + Self::Multiple(others) => { + if !others.contains(val) { + *self = CandidateValue::Impossible; + } + } + Self::All => {} // self is unchanged. + } + } + Self::Multiple(multiple) => { + match other { + Self::Impossible => *self = CandidateValue::Impossible, + Self::Single(other) => { + // The other side can only be a single value. + // The result is either only a single value or impossible + // depending on whether there's overlap. + if multiple.contains(&other) { + *self = Self::Single(other); + } else { + *self = Self::Impossible; + } + } + Self::Multiple(others) => { + multiple.retain(|value| others.contains(value)); + let possibilities = multiple.len(); + if possibilities == 0 { + *self = Self::Impossible; + } else if possibilities == 1 { + let first = multiple.swap_remove(0); + *self = Self::Single(first); + } // otherwise it stays Multiple and we already mutated the Vec it holds + } + Self::All => {} // self is unchanged. + } + } + Self::All => { + // Whatever the other candidate was. It can't be any wider than Self::All. + *self = other; + } + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Range { + start: Bound, + end: Bound, +} + +impl Range { + pub const FULL: Self = Self { + start: Bound::Unbounded, + end: Bound::Unbounded, + }; + + pub(super) fn new(start: Bound, end: Bound) -> Self { + Self { start, end } + } + + pub(super) fn with_start(start: Bound) -> Self { + Self { + start, + end: Bound::Unbounded, + } + } + + pub(super) fn with_end(end: Bound) -> Self { + Self { + start: Bound::Unbounded, + end, + } + } +} + +impl RangeBounds for Range { + fn start_bound(&self) -> Bound<&FieldValue> { + self.start.as_ref() + } + + fn end_bound(&self) -> Bound<&FieldValue> { + self.end.as_ref() + } +} + +#[cfg(test)] +mod tests { + use crate::ir::FieldValue; + + use super::CandidateValue; + + #[test] + fn test_candidate_merging() { + use CandidateValue::*; + let one = FieldValue::Int64(1); + let two = FieldValue::Int64(2); + let three = FieldValue::Int64(3); + let four = FieldValue::Int64(4); + + let test_cases = [ + // Anything merged into Impossible is Impossible. + (Impossible, Impossible, Impossible), + (Impossible, Single(&one), Impossible), + (Impossible, Multiple(vec![&one, &two]), Impossible), + // + // Merging Impossible into anything produces Imposssible. + (Single(&one), Impossible, Impossible), + (Multiple(vec![&one, &two]), Impossible, Impossible), + // + // Merging null into non-null, or vice versa, produces Impossible. + (Single(&FieldValue::NULL), Single(&one), Impossible), + ( + Single(&FieldValue::NULL), + Multiple(vec![&one, &two]), + Impossible, + ), + (Single(&one), Single(&FieldValue::NULL), Impossible), + ( + Multiple(vec![&one, &two]), + Single(&FieldValue::NULL), + Impossible, + ), + // + // Merging non-overlapping single or multiple values produces Impossible. + (Single(&one), Single(&two), Impossible), + (Single(&one), Multiple(vec![&two, &three]), Impossible), + (Multiple(vec![&one, &two]), Single(&three), Impossible), + ( + Multiple(vec![&one, &two]), + Multiple(vec![&three, &four]), + Impossible, + ), + // + // Merging overlapping single values, or single with multiple, + // produces the overlapping Single. + (Single(&one), Single(&one), Single(&one)), + (Multiple(vec![&one, &two]), Single(&one), Single(&one)), + (Single(&one), Multiple(vec![&one, &two]), Single(&one)), + // + // Merging null into multiple that contains null produces null. + ( + Single(&FieldValue::NULL), + Multiple(vec![&one, &FieldValue::Null]), + Single(&FieldValue::NULL), + ), + // + // Merging multiple values that include null works correctly too. + ( + Multiple(vec![&one, &FieldValue::Null, &two, &three]), + Multiple(vec![&one, &FieldValue::Null, &four]), + Multiple(vec![&one, &FieldValue::Null]), + ), + // + // Merging overlapping multiple values can produce either a Single or a Multiple, + // depending on the overlap size. + ( + Multiple(vec![&one, &two]), + Multiple(vec![&two, &three]), + Single(&two), + ), + ( + Multiple(vec![&two, &three]), + Multiple(vec![&one, &two]), + Single(&two), + ), + ( + Multiple(vec![&one, &two, &three, &four]), + Multiple(vec![&two, &three]), + Multiple(vec![&two, &three]), + ), + ( + Multiple(vec![&two, &three]), + Multiple(vec![&one, &two, &three, &four]), + Multiple(vec![&two, &three]), + ), + // + // Merging multiple overlapping values preserves the order of the original. + ( + Multiple(vec![&one, &two, &three, &four]), + Multiple(vec![&three, &two]), + Multiple(vec![&two, &three]), + ), + // + // Merging Candidate::All from either position produces whatever the other value was. + (All, Impossible, Impossible), + (Impossible, All, Impossible), + (All, Single(&one), Single(&one)), + (Single(&one), All, Single(&one)), + (All, Multiple(vec![&one, &two]), Multiple(vec![&one, &two])), + (Multiple(vec![&one, &two]), All, Multiple(vec![&one, &two])), + (All, All, All), + ]; + + for (original, merged, expected) in test_cases { + let mut base = original.clone(); + base.merge(merged.clone()); + assert_eq!( + expected, base, + "{original:?} + {merged:?} = {base:?} != {expected:?}" + ); + } + } +} diff --git a/trustfall_core/src/interpreter/hints/constraint.rs b/trustfall_core/src/interpreter/hints/constraint.rs new file mode 100644 index 00000000..dceb11e1 --- /dev/null +++ b/trustfall_core/src/interpreter/hints/constraint.rs @@ -0,0 +1,209 @@ +use std::{fmt::Debug, ops::Bound, sync::Arc}; + +use crate::{ + interpreter::{ + execution::compute_context_field_with_separate_value, Adapter, ContextIterator, + ContextOutcomeIterator, QueryInfo, + }, + ir::{ContextField, FieldValue, IRQueryComponent}, +}; + +use super::{CandidateValue, Range}; + +#[non_exhaustive] +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct DynamicConstraint { + resolve_on_component: Arc, + field: ContextField, + content: C, +} + +impl DynamicConstraint { + pub(super) fn new( + resolve_on_component: Arc, + field: ContextField, + content: C, + ) -> Self { + Self { + resolve_on_component, + field, + content, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum BoundKind { + LessThan, + LessThanOrEqual, + GreaterThan, + GreaterThanOrEqual, +} + +impl BoundKind { + fn make_range(&self, value: FieldValue) -> Range { + match self { + BoundKind::LessThan => Range::with_end(Bound::Excluded(value)), + BoundKind::LessThanOrEqual => Range::with_end(Bound::Included(value)), + BoundKind::GreaterThan => Range::with_start(Bound::Excluded(value)), + BoundKind::GreaterThanOrEqual => Range::with_start(Bound::Included(value)), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) struct CandidateInfo { + pub(super) is_multiple: bool, +} + +impl CandidateInfo { + pub(super) fn new(is_multiple: bool) -> Self { + Self { is_multiple } + } +} + +#[non_exhaustive] +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DynamicallyResolvedValue { + query_info: QueryInfo, + constraint: DynamicConstraint, +} + +impl DynamicallyResolvedValue { + pub(super) fn new(query_info: QueryInfo, constraint: DynamicConstraint) -> Self { + Self { + query_info, + constraint, + } + } + + pub fn resolve< + 'vertex, + VertexT: Debug + Clone + 'vertex, + AdapterT: Adapter<'vertex, Vertex = VertexT>, + >( + mut self, + adapter: &mut AdapterT, + contexts: ContextIterator<'vertex, VertexT>, + ) -> ContextOutcomeIterator<'vertex, VertexT, CandidateValue> { + let iterator = compute_context_field_with_separate_value( + adapter, + &mut self.query_info, + &self.constraint.resolve_on_component, + &self.constraint.field, + contexts, + ); + let context_field_vid = self.constraint.field.vertex_id; + let nullable_context_field = self.constraint.field.field_type.nullable; + if self.constraint.content.is_multiple { + Box::new(iterator.map(move |(ctx, value)| { + match value { + FieldValue::List(v) => (ctx, CandidateValue::Multiple(v)), + FieldValue::Null => { + // Either a nullable field was tagged, or + // the @tag is inside an @optional scope that doesn't exist. + let candidate = if ctx.vertices[&context_field_vid].is_none() { + // @optional scope that didn't exist. Our query rules say that + // any filters using this tag *must* pass. + CandidateValue::All + } else { + // The field must have been nullable. + debug_assert!( + nullable_context_field, + "tagged field {:?} was not nullable but received a null value for it", + self.constraint.field, + ); + CandidateValue::Impossible + }; + (ctx, candidate) + } + bad_value => { + panic!( + "\ +field {} of type {:?} produced an invalid value when resolving @tag: {bad_value:?}", + self.constraint.field.field_name, self.constraint.field.field_type, + ) + } + } + })) + } else { + Box::new(iterator.map(move |(ctx, value)| match value { + null_value @ FieldValue::Null => { + // Either a nullable field was tagged, or + // the @tag is inside an @optional scope that doesn't exist. + let candidate = if ctx.vertices[&context_field_vid].is_none() { + // @optional scope that didn't exist. Our query rules say that + // any filters using this tag *must* pass. + CandidateValue::All + } else { + // The field must have been nullable. + debug_assert!( + nullable_context_field, + "tagged field {:?} was not nullable but received a null value for it", + self.constraint.field, + ); + CandidateValue::Single(null_value) + }; + (ctx, candidate) + } + other_value => (ctx, CandidateValue::Single(other_value)), + })) + } + } +} + +#[non_exhaustive] +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DynamicallyResolvedRange { + info: QueryInfo, + constraint: DynamicConstraint, +} + +impl DynamicallyResolvedRange { + pub(super) fn new(info: QueryInfo, constraint: DynamicConstraint) -> Self { + Self { info, constraint } + } + + pub fn resolve< + 'vertex, + VertexT: Debug + Clone + 'vertex, + AdapterT: Adapter<'vertex, Vertex = VertexT>, + >( + mut self, + adapter: &mut AdapterT, + contexts: ContextIterator<'vertex, VertexT>, + ) -> ContextOutcomeIterator<'vertex, VertexT, Range> { + let iterator = compute_context_field_with_separate_value( + adapter, + &mut self.info, + &self.constraint.resolve_on_component, + &self.constraint.field, + contexts, + ); + let context_field_vid = self.constraint.field.vertex_id; + let nullable_context_field = self.constraint.field.field_type.nullable; + let bound = self.constraint.content; + + Box::new(iterator.map(move |(ctx, value)| match value { + FieldValue::Null => { + // Either a nullable field was tagged, or + // the @tag is inside an @optional scope that doesn't exist. + let range = if ctx.vertices[&context_field_vid].is_none() { + // @optional scope that didn't exist. Our query rules say that + // any filters using this tag *must* pass. + Range::FULL + } else { + // The field must have been nullable. + debug_assert!( + nullable_context_field, + "tagged field {:?} was not nullable but received a null value for it", + self.constraint.field, + ); + bound.make_range(FieldValue::Null) + }; + (ctx, range) + } + other_value => (ctx, bound.make_range(other_value)), + })) + } +} diff --git a/trustfall_core/src/interpreter/hints/mod.rs b/trustfall_core/src/interpreter/hints/mod.rs index e0343e57..478e5f9d 100644 --- a/trustfall_core/src/interpreter/hints/mod.rs +++ b/trustfall_core/src/interpreter/hints/mod.rs @@ -1,16 +1,129 @@ -use std::{collections::BTreeMap, sync::Arc}; +#![allow(unused_variables, dead_code, unreachable_code)] -use crate::ir::{Eid, FieldValue, IRQuery, Vid}; +use std::collections::BTreeMap; +use std::fmt::Debug; +use std::sync::Arc; + +use crate::ir::indexed::Output; +use crate::ir::{Argument, FieldRef, IREdge, IRFold, IRQuery, IRVertex, Operation, Recursive}; +use crate::ir::{Eid, FieldValue, IRQueryComponent, Vid}; + +use self::constraint::{ + CandidateInfo, DynamicConstraint, DynamicallyResolvedRange, DynamicallyResolvedValue, +}; + +mod candidates; +mod constraint; + +pub use candidates::{CandidateValue, Range}; use super::InterpretedQuery; +/// Information about what some query is looking for at a specific vertex in the query structure. +#[cfg_attr(docsrs, doc(notable_trait))] +pub trait VertexInfo { + /// Queries may consist of multiple components. + /// This is the component in which the vertex represented by this value is located. + fn current_component(&self) -> &IRQueryComponent; + + /// The query IR of the vertex represented by this value. + fn current_vertex(&self) -> &IRVertex; + + /// The arguments with which the query was executed. + fn query_arguments(&self) -> &BTreeMap, FieldValue>; + + /// The type coercion the query applied at this vertex, if any. + fn coerced_to_type(&self) -> Option<&Arc> { + let vertex = self.current_vertex(); + if vertex.coerced_from_type.is_some() { + Some(&vertex.type_name) + } else { + None + } + } + + fn static_field_value(&self, field_name: &str) -> Option> { + let vertex = self.current_vertex(); + + let is_null = vertex + .filters + .iter() + .any(|op| matches!(op, Operation::IsNull(..))); + let is_not_null = vertex + .filters + .iter() + .any(|op| matches!(op, Operation::IsNotNull(..))); + + if is_null && is_not_null { + // The value can't be both null and non-null at the same time. + return Some(CandidateValue::Impossible); + } + + let mut candidate = if is_null { + Some(CandidateValue::Single(&FieldValue::NULL)) + } else { + None + }; + + let arguments = self.query_arguments(); + for filter_operation in &vertex.filters { + match filter_operation { + Operation::Equals(_, Argument::Variable(var)) => { + let value = &arguments[&var.variable_name]; + if let Some(candidate) = candidate.as_mut() { + candidate.merge(CandidateValue::Single(value)); + } else { + candidate = Some(CandidateValue::Single(value)); + } + } + Operation::OneOf(_, Argument::Variable(var)) => { + let values: Vec<&FieldValue> = arguments[&var.variable_name] + .as_vec() + .expect("OneOf operation using a non-vec FieldValue") + .iter() + .map(AsRef::as_ref) + .collect(); + if let Some(candidate) = candidate.as_mut() { + candidate.merge(CandidateValue::Multiple(values)); + } else { + candidate = Some(CandidateValue::Multiple(values)); + } + } + _ => {} + } + } + + candidate + } + + fn static_field_range(&self, field_name: &str) -> Option<&Range> { + todo!() + } + + /// Only the first matching `@tag` value is returned. + fn dynamic_field_value(&self, field_name: &str) -> Option; + + fn dynamic_field_range(&self, field_name: &str) -> Option { + todo!() + } + + // non-optional, non-recursed, non-folded edge + // TODO: What happens if the same edge exists more than once in a given scope? + fn first_required_edge(&self, edge_name: &str) -> Option; + + // optional, recursed, or folded edge; + // recursed because recursion always starts at depth 0 + // TODO: What happens if the same edge exists more than once in a given scope? + fn first_edge(&self, edge_name: &str) -> Option; +} + /// Information about the query being processed. #[non_exhaustive] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct QueryInfo { query: InterpretedQuery, - current_vertex: Vid, - crossing_eid: Option, + pub(crate) current_vertex: Vid, + pub(crate) crossing_eid: Option, } impl QueryInfo { @@ -26,12 +139,14 @@ impl QueryInfo { } } - #[allow(dead_code)] pub(crate) fn ir_query(&self) -> &IRQuery { &self.query.indexed_query.ir_query } - #[allow(dead_code)] + pub(crate) fn outputs(&self) -> &BTreeMap, Output> { + &self.query.indexed_query.outputs + } + pub(crate) fn arguments(&self) -> &Arc, FieldValue>> { &self.query.arguments } @@ -45,4 +160,361 @@ impl QueryInfo { pub fn origin_crossing_eid(&self) -> Option { self.crossing_eid } + + #[inline] + pub fn here(&self) -> LocalQueryInfo { + LocalQueryInfo { + query_info: self.clone(), + current_vertex: self.current_vertex, + } + } + + #[inline] + pub fn destination(&self) -> Option { + self.crossing_eid.map(|eid| { + let current_vertex = match &self.query.indexed_query.eids[&eid] { + crate::ir::indexed::EdgeKind::Regular(regular) => regular.to_vid, + crate::ir::indexed::EdgeKind::Fold(fold) => fold.to_vid, + }; + LocalQueryInfo { + query_info: self.clone(), + current_vertex, + } + }) + } +} + +#[non_exhaustive] +#[derive(Debug, Clone)] +pub struct LocalQueryInfo { + query_info: QueryInfo, + current_vertex: Vid, +} + +impl LocalQueryInfo { + fn make_non_folded_edge_info(&self, edge: &IREdge) -> EdgeInfo { + let neighboring_info = NeighboringQueryInfo { + query_info: self.query_info.clone(), + starting_vertex: self.current_vertex, + neighbor_vertex: edge.to_vid, + neighbor_path: vec![edge.eid], + }; + EdgeInfo { + eid: edge.eid, + optional: edge.optional, + recursive: edge.recursive.clone(), + folded: false, + destination: neighboring_info, + } + } + + fn make_folded_edge_info(&self, fold: &IRFold) -> EdgeInfo { + let neighboring_info = NeighboringQueryInfo { + query_info: self.query_info.clone(), + starting_vertex: self.current_vertex, + neighbor_vertex: fold.to_vid, + neighbor_path: vec![fold.eid], + }; + EdgeInfo { + eid: fold.eid, + optional: true, + recursive: None, + folded: true, + destination: neighboring_info, + } + } +} + +impl VertexInfo for LocalQueryInfo { + #[inline] + fn current_vertex(&self) -> &IRVertex { + &self.current_component().vertices[&self.current_vertex] + } + + #[inline] + fn current_component(&self) -> &IRQueryComponent { + &self.query_info.query.indexed_query.vids[&self.current_vertex] + } + + #[inline] + fn query_arguments(&self) -> &BTreeMap, FieldValue> { + self.query_info.arguments() + } + + fn dynamic_field_value(&self, field_name: &str) -> Option { + let vertex = self.current_vertex(); + for filter_operation in &vertex.filters { + match filter_operation { + // TODO: handle tags of fold-specific fields + Operation::Equals(_, Argument::Tag(FieldRef::ContextField(context_field))) => { + return Some(DynamicallyResolvedValue::new( + self.query_info.clone(), + DynamicConstraint::new( + self.query_info.query.indexed_query.vids[&vertex.vid].clone(), + context_field.clone(), + CandidateInfo::new(false), + ), + )); + } + Operation::OneOf(_, Argument::Tag(FieldRef::ContextField(context_field))) => { + return Some(DynamicallyResolvedValue::new( + self.query_info.clone(), + DynamicConstraint::new( + self.query_info.query.indexed_query.vids[&vertex.vid].clone(), + context_field.clone(), + CandidateInfo::new(true), + ), + )); + } + _ => {} + } + } + + None + } + + // fn dynamic_field_range(&self, field_name: &str) -> Option> { + // todo!() + // } + + // non-optional, non-recursed, non-folded edge + fn first_required_edge(&self, edge_name: &str) -> Option { + // TODO: What happens if the same edge exists more than once in a given scope? + let component = self.current_component(); + let current_vertex = self.current_vertex(); + let first_matching_edge = component.edges.values().find(|edge| { + edge.from_vid == current_vertex.vid + && !edge.optional + && edge.recursive.is_none() + && edge.edge_name.as_ref() == edge_name + }); + first_matching_edge.map(|edge| self.make_non_folded_edge_info(edge.as_ref())) + } + + fn first_edge(&self, edge_name: &str) -> Option { + // TODO: What happens if the same edge exists more than once in a given scope? + let component = self.current_component(); + let current_vertex = self.current_vertex(); + let first_matching_edge = component.edges.values().find(|edge| { + edge.from_vid == current_vertex.vid && edge.edge_name.as_ref() == edge_name + }); + first_matching_edge + .map(|edge| self.make_non_folded_edge_info(edge.as_ref())) + .or_else(|| { + component + .folds + .values() + .find(|fold| { + fold.from_vid == current_vertex.vid && fold.edge_name.as_ref() == edge_name + }) + .map(|fold| self.make_folded_edge_info(fold.as_ref())) + }) + } +} + +#[non_exhaustive] +#[derive(Debug, Clone)] +pub struct EdgeInfo { + eid: Eid, + optional: bool, + recursive: Option, + folded: bool, + destination: NeighboringQueryInfo, +} + +impl EdgeInfo { + pub fn destination(&self) -> &NeighboringQueryInfo { + &self.destination + } +} + +#[non_exhaustive] +#[derive(Debug, Clone)] +pub struct NeighboringQueryInfo { + query_info: QueryInfo, + starting_vertex: Vid, + neighbor_vertex: Vid, + neighbor_path: Vec, +} + +impl NeighboringQueryInfo { + fn make_non_folded_edge_info(&self, edge: &IREdge) -> EdgeInfo { + let mut neighbor_path = self.neighbor_path.clone(); + neighbor_path.push(edge.eid); + let neighboring_info = NeighboringQueryInfo { + query_info: self.query_info.clone(), + starting_vertex: self.starting_vertex, + neighbor_vertex: edge.to_vid, + neighbor_path, + }; + EdgeInfo { + eid: edge.eid, + optional: edge.optional, + recursive: edge.recursive.clone(), + folded: false, + destination: neighboring_info, + } + } + + fn make_folded_edge_info(&self, fold: &IRFold) -> EdgeInfo { + let mut neighbor_path = self.neighbor_path.clone(); + neighbor_path.push(fold.eid); + let neighboring_info = NeighboringQueryInfo { + query_info: self.query_info.clone(), + starting_vertex: self.starting_vertex, + neighbor_vertex: fold.to_vid, + neighbor_path, + }; + EdgeInfo { + eid: fold.eid, + optional: true, + recursive: None, + folded: true, + destination: neighboring_info, + } + } +} + +impl VertexInfo for NeighboringQueryInfo { + #[inline] + fn current_vertex(&self) -> &IRVertex { + &self.current_component().vertices[&self.neighbor_vertex] + } + + #[inline] + fn current_component(&self) -> &IRQueryComponent { + &self.query_info.query.indexed_query.vids[&self.neighbor_vertex] + } + + #[inline] + fn query_arguments(&self) -> &BTreeMap, FieldValue> { + self.query_info.arguments() + } + + fn dynamic_field_value(&self, field_name: &str) -> Option { + let vertex = self.current_vertex(); + + for filter_operation in &vertex.filters { + // Before deciding that some tag matches, we have to check if it corresponds + // to field whose vertex has already been resolved. + // + // Here's an example where this is important: + // { + // Foo { + // bar { + // number @tag @output + // baz { + // target @filter(op: "=", value: ["%number"]) + // } + // } + // } + // } + // Imagine execution is currently at `Foo`, and the adapter checked whether + // the `target` property at neighbor path `-> bar -> baz` has known values. + // Despite the use of `%number` on that property, the answer is "no" -- + // the value isn't known *yet* at the point of query evaluation of the caller. + // + // This is why we ensure that the tagged value came from a Vid that is at or before + // the Vid where the caller currently stands. + match filter_operation { + // TODO: handle tags of fold-specific fields + Operation::Equals(_, Argument::Tag(FieldRef::ContextField(context_field))) => { + if context_field.vertex_id <= self.starting_vertex { + return Some(DynamicallyResolvedValue::new( + self.query_info.clone(), + DynamicConstraint::new( + self.query_info.query.indexed_query.vids[&self.starting_vertex] + .clone(), + context_field.clone(), + CandidateInfo::new(false), + ), + )); + } + } + Operation::OneOf(_, Argument::Tag(FieldRef::ContextField(context_field))) => { + if context_field.vertex_id <= self.starting_vertex { + return Some(DynamicallyResolvedValue::new( + self.query_info.clone(), + DynamicConstraint::new( + self.query_info.query.indexed_query.vids[&self.starting_vertex] + .clone(), + context_field.clone(), + CandidateInfo::new(true), + ), + )); + } + } + _ => {} + } + } + + None + } + + // fn dynamic_field_range(&self, field_name: &str) -> Option> { + // todo!() + // } + + fn first_required_edge(&self, edge_name: &str) -> Option { + // TODO: What happens if the same edge exists more than once in a given scope? + let component = self.current_component(); + let current_vertex = self.current_vertex(); + let first_matching_edge = component.edges.values().find(|edge| { + edge.from_vid == current_vertex.vid + && !edge.optional + && edge.recursive.is_none() + && edge.edge_name.as_ref() == edge_name + }); + first_matching_edge.map(|edge| self.make_non_folded_edge_info(edge.as_ref())) + } + + fn first_edge(&self, edge_name: &str) -> Option { + // TODO: What happens if the same edge exists more than once in a given scope? + let component = self.current_component(); + let current_vertex = self.current_vertex(); + let first_matching_edge = component.edges.values().find(|edge| { + edge.from_vid == current_vertex.vid && edge.edge_name.as_ref() == edge_name + }); + first_matching_edge + .map(|edge| self.make_non_folded_edge_info(edge.as_ref())) + .or_else(|| { + component + .folds + .values() + .find(|fold| { + fold.from_vid == current_vertex.vid && fold.edge_name.as_ref() == edge_name + }) + .map(|fold| self.make_folded_edge_info(fold.as_ref())) + }) + } +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use serde::{Deserialize, Serialize}; + + use crate::ir::{FieldValue, Vid}; + + use super::Range; + + #[derive(Debug)] + struct TestAdapter { + test_data: HintTestData, + } + + #[derive(Debug, Serialize, Deserialize)] + struct HintTestData { + expected_field_info: BTreeMap>, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + struct ExpectedFieldInfo { + field_name: String, + static_value: Option, + dynamic_value: Option>, + static_range: Option, + dynamic_range: Option>, + } } diff --git a/trustfall_core/src/interpreter/mod.rs b/trustfall_core/src/interpreter/mod.rs index a1d498b9..59a1c85c 100644 --- a/trustfall_core/src/interpreter/mod.rs +++ b/trustfall_core/src/interpreter/mod.rs @@ -23,7 +23,7 @@ mod hints; pub mod replay; pub mod trace; -pub use hints::QueryInfo; +pub use hints::{CandidateValue, QueryInfo, VertexInfo}; /// An iterator of vertices representing data points we are querying. pub type VertexIterator<'vertex, VertexT> = Box + 'vertex>; diff --git a/trustfall_core/src/ir/value.rs b/trustfall_core/src/ir/value.rs index 3def9ab7..1872c7c9 100644 --- a/trustfall_core/src/ir/value.rs +++ b/trustfall_core/src/ir/value.rs @@ -26,6 +26,10 @@ pub enum FieldValue { List(Vec), } +impl FieldValue { + pub const NULL: Self = FieldValue::Null; +} + /// Values of fields in GraphQL types. /// /// Same as [FieldValue], but serialized as an untagged enum, @@ -142,7 +146,17 @@ impl FieldValue { } } - pub fn as_vec<'a, T>(&'a self, inner: impl Fn(&'a FieldValue) -> Option) -> Option> { + pub fn as_vec(&self) -> Option<&Vec> { + match self { + FieldValue::List(l) => Some(l), + _ => None, + } + } + + pub fn as_vec_with<'a, T>( + &'a self, + inner: impl Fn(&'a FieldValue) -> Option, + ) -> Option> { match self { FieldValue::List(l) => { let maybe_vec: Option> = l.iter().map(inner).collect(); diff --git a/trustfall_core/src/lib.rs b/trustfall_core/src/lib.rs index 180c10b9..0d3107fe 100644 --- a/trustfall_core/src/lib.rs +++ b/trustfall_core/src/lib.rs @@ -1,6 +1,7 @@ #![forbid(unsafe_code)] #![forbid(unused_lifetimes)] #![allow(clippy::result_large_err)] // TODO: clean this up repo-wide +#![cfg_attr(docsrs, feature(doc_notable_trait))] #[macro_use] extern crate maplit;