trustfall_core/frontend/
mod.rs

1//! Frontend for Trustfall: takes a parsed query, validates it, and turns it into IR.
2#![allow(dead_code, unused_variables, unused_mut)]
3use std::{collections::BTreeMap, iter::successors, num::NonZeroUsize, sync::Arc};
4
5use async_graphql_parser::{
6    types::{ExecutableDocument, FieldDefinition, TypeDefinition, TypeKind},
7    Positioned,
8};
9use filters::make_filter_expr;
10use smallvec::SmallVec;
11
12use crate::{
13    graphql_query::{
14        directives::{FilterDirective, FoldGroup, RecurseDirective},
15        query::{parse_document, FieldConnection, FieldNode, Query},
16    },
17    ir::{
18        get_typename_meta_field, Argument, ContextField, EdgeParameters, Eid, FieldRef, FieldValue,
19        FoldSpecificField, FoldSpecificFieldKind, IREdge, IRFold, IRQuery, IRQueryComponent,
20        IRVertex, IndexedQuery, LocalField, Operation, Recursive, TransformationKind, Type, Vid,
21        TYPENAME_META_FIELD,
22    },
23    schema::{get_builtin_scalars, FieldOrigin, Schema},
24    util::{BTreeMapTryInsertExt, TryCollectUniqueKey},
25};
26
27use self::{
28    error::{DuplicatedNamesConflict, FilterTypeError, FrontendError, ValidationError},
29    outputs::OutputHandler,
30    tags::TagHandler,
31    util::{get_underlying_named_type, ComponentPath},
32    validation::validate_query_against_schema,
33};
34
35pub mod error;
36mod filters;
37mod outputs;
38mod tags;
39mod util;
40mod validation;
41
42/// Parses a query string to the Trustfall IR using a provided
43/// [Schema]. May fail if [parse_to_ir] fails for the provided schema and query.
44pub fn parse(schema: &Schema, query: impl AsRef<str>) -> Result<Arc<IndexedQuery>, FrontendError> {
45    let ir_query = parse_to_ir(schema, query)?;
46
47    // .unwrap() must be safe here, since freshly-generated IRQuery objects must always
48    // be safe to convert to IndexedQuery. This is a try_into() instead of into() because
49    // IRQuery is Serialize/Deserialize and may therefore have been edited (e.g. by hand)
50    // before being converted into IndexedQuery.
51    let indexed_query: IndexedQuery = ir_query.try_into().unwrap();
52
53    Ok(Arc::from(indexed_query))
54}
55
56/// Parses a query string to IR using a [Schema].
57pub fn parse_to_ir<T: AsRef<str>>(schema: &Schema, query: T) -> Result<IRQuery, FrontendError> {
58    let document = async_graphql_parser::parse_query(query)?;
59    let q = parse_document(&document)?;
60    make_ir_for_query(schema, &q)
61}
62
63pub fn parse_doc(schema: &Schema, document: &ExecutableDocument) -> Result<IRQuery, FrontendError> {
64    let q = parse_document(document)?;
65    make_ir_for_query(schema, &q)
66}
67
68fn get_field_name_and_type_from_schema<'a>(
69    defined_fields: &'a [Positioned<FieldDefinition>],
70    field_node: &FieldNode,
71) -> (&'a str, Arc<str>, Arc<str>, Type) {
72    if field_node.name.as_ref() == TYPENAME_META_FIELD {
73        let field_name = get_typename_meta_field();
74        return (
75            TYPENAME_META_FIELD,
76            field_name.clone(),
77            field_name.clone(),
78            Type::new_named_type("String", false),
79        );
80    }
81
82    for defined_field in defined_fields {
83        let field_name = &defined_field.node.name.node;
84        let field_raw_type = &defined_field.node.ty.node;
85        if field_name.as_ref() == field_node.name.as_ref() {
86            let pre_coercion_type_name: Arc<str> =
87                get_underlying_named_type(field_raw_type).to_string().into();
88            let post_coercion_type_name = if let Some(coerced_to) = &field_node.coerced_to {
89                coerced_to.clone()
90            } else {
91                pre_coercion_type_name.clone()
92            };
93            return (
94                field_name,
95                pre_coercion_type_name,
96                post_coercion_type_name,
97                Type::from_type(field_raw_type),
98            );
99        }
100    }
101
102    unreachable!()
103}
104
105fn get_vertex_type_definition_from_schema<'a>(
106    schema: &'a Schema,
107    vertex_type_name: &str,
108) -> Result<&'a TypeDefinition, FrontendError> {
109    schema.vertex_types.get(vertex_type_name).ok_or_else(|| {
110        FrontendError::ValidationError(ValidationError::NonExistentType(
111            vertex_type_name.to_owned(),
112        ))
113    })
114}
115
116fn get_edge_definition_from_schema<'a>(
117    schema: &'a Schema,
118    type_name: &str,
119    edge_name: &str,
120) -> &'a FieldDefinition {
121    let defined_fields = get_vertex_field_definitions(schema, type_name);
122
123    for defined_field in defined_fields {
124        let field_name = defined_field.node.name.node.as_str();
125        if field_name == edge_name {
126            return &defined_field.node;
127        }
128    }
129
130    unreachable!()
131}
132
133fn get_vertex_field_definitions<'a>(
134    schema: &'a Schema,
135    type_name: &str,
136) -> &'a Vec<Positioned<FieldDefinition>> {
137    match &schema.vertex_types[type_name].kind {
138        TypeKind::Object(o) => &o.fields,
139        TypeKind::Interface(i) => &i.fields,
140        _ => unreachable!(),
141    }
142}
143
144fn make_edge_parameters(
145    edge_definition: &FieldDefinition,
146    specified_arguments: &BTreeMap<Arc<str>, FieldValue>,
147) -> Result<EdgeParameters, Vec<FrontendError>> {
148    let mut errors: Vec<FrontendError> = vec![];
149
150    let mut edge_arguments: BTreeMap<Arc<str>, FieldValue> = BTreeMap::new();
151    for arg in &edge_definition.arguments {
152        let arg_name = arg.node.name.node.as_ref();
153        let specified_value = match specified_arguments.get(arg_name) {
154            None => {
155                // Argument value was not specified.
156                // If there's an explicit default defined in the schema, use it.
157                // Otherwise, if the parameter is nullable, use an implicit "null" default.
158                // All other cases are an error.
159                arg.node
160                    .default_value
161                    .as_ref()
162                    .map(|v| {
163                        let value = FieldValue::try_from(v.node.clone()).unwrap();
164
165                        // The default value must be a valid type for the parameter,
166                        // otherwise the schema itself is invalid.
167                        assert!(Type::from_type(&arg.node.ty.node).is_valid_value(&value));
168
169                        value
170                    })
171                    .or({
172                        if arg.node.ty.node.nullable {
173                            Some(FieldValue::Null)
174                        } else {
175                            None
176                        }
177                    })
178            }
179            Some(value) => {
180                // Type-check the supplied value against the schema.
181                if !Type::from_type(&arg.node.ty.node).is_valid_value(value) {
182                    errors.push(FrontendError::InvalidEdgeParameterType(
183                        arg_name.to_string(),
184                        edge_definition.name.node.to_string(),
185                        arg.node.ty.to_string(),
186                        value.clone(),
187                    ));
188                }
189                Some(value.clone())
190            }
191        };
192
193        match specified_value {
194            None => {
195                errors.push(FrontendError::MissingRequiredEdgeParameter(
196                    arg_name.to_string(),
197                    edge_definition.name.node.to_string(),
198                ));
199            }
200            Some(value) => {
201                edge_arguments.insert_or_error(arg_name.to_owned().into(), value).unwrap();
202                // Duplicates should have been caught at parse time.
203            }
204        }
205    }
206
207    // Check whether any of the supplied parameters aren't expected by the schema.
208    for specified_argument_name in specified_arguments.keys() {
209        if !edge_arguments.contains_key(specified_argument_name) {
210            // This edge parameter isn't defined expected in the schema,
211            // and it's an error to supply it.
212            errors.push(FrontendError::UnexpectedEdgeParameter(
213                specified_argument_name.to_string(),
214                edge_definition.name.node.to_string(),
215            ))
216        }
217    }
218
219    if !errors.is_empty() {
220        Err(errors)
221    } else {
222        Ok(EdgeParameters::new(Arc::new(edge_arguments)))
223    }
224}
225
226#[allow(clippy::too_many_arguments)]
227fn make_local_field_filter_expr(
228    schema: &Schema,
229    component_path: &ComponentPath,
230    tags: &mut TagHandler<'_>,
231    current_vertex_vid: Vid,
232    property_name: &Arc<str>,
233    property_type: &Type,
234    filter_directive: &FilterDirective,
235) -> Result<Operation<LocalField, Argument>, Vec<FrontendError>> {
236    let left = LocalField { field_name: property_name.clone(), field_type: property_type.clone() };
237
238    filters::make_filter_expr(
239        schema,
240        component_path,
241        tags,
242        current_vertex_vid,
243        left,
244        filter_directive,
245    )
246}
247
248pub fn make_ir_for_query(schema: &Schema, query: &Query) -> Result<IRQuery, FrontendError> {
249    validate_query_against_schema(schema, query)?;
250
251    let mut vid_maker = successors(Some(Vid::new(NonZeroUsize::new(1).unwrap())), |x| {
252        let inner_number = x.0.get();
253        Some(Vid::new(NonZeroUsize::new(inner_number.checked_add(1).unwrap()).unwrap()))
254    });
255    let mut eid_maker = successors(Some(Eid::new(NonZeroUsize::new(1).unwrap())), |x| {
256        let inner_number = x.0.get();
257        Some(Eid::new(NonZeroUsize::new(inner_number.checked_add(1).unwrap()).unwrap()))
258    });
259
260    let mut errors: Vec<FrontendError> = vec![];
261
262    let (root_field_name, root_field_pre_coercion_type, root_field_post_coercion_type, _) =
263        get_field_name_and_type_from_schema(&schema.query_type.fields, &query.root_field);
264    let starting_vid = vid_maker.next().unwrap();
265
266    let root_parameters = make_edge_parameters(
267        get_edge_definition_from_schema(schema, schema.query_type_name(), root_field_name),
268        &query.root_connection.arguments,
269    );
270
271    let mut component_path = ComponentPath::new(starting_vid);
272    let mut tags = Default::default();
273    let mut output_handler = OutputHandler::new(starting_vid, None);
274    let mut root_component = make_query_component(
275        schema,
276        query,
277        &mut vid_maker,
278        &mut eid_maker,
279        &mut component_path,
280        &mut output_handler,
281        &mut tags,
282        None,
283        starting_vid,
284        root_field_pre_coercion_type,
285        root_field_post_coercion_type,
286        &query.root_field,
287    );
288
289    if let Err(e) = &root_parameters {
290        errors.extend(e.iter().cloned());
291    }
292
293    let root_component = match root_component {
294        Ok(r) => r,
295        Err(e) => {
296            errors.extend(e);
297            return Err(errors.into());
298        }
299    };
300    let mut variables: BTreeMap<Arc<str>, Type> = Default::default();
301    if let Err(v) = fill_in_query_variables(&mut variables, &root_component) {
302        errors.extend(v.into_iter().map(|x| x.into()));
303    }
304
305    if let Err(e) = tags.finish() {
306        errors.push(FrontendError::UnusedTags(e.into_iter().map(String::from).collect()));
307    }
308
309    let all_outputs = output_handler.finish();
310    if let Err(duplicates) = check_for_duplicate_output_names(all_outputs) {
311        let all_vertices = collect_ir_vertices(&root_component);
312        let errs = make_duplicated_output_names_error(&all_vertices, duplicates);
313        errors.extend(errs);
314    }
315
316    if errors.is_empty() {
317        Ok(IRQuery {
318            root_name: root_field_name.into(),
319            root_parameters: root_parameters.unwrap(),
320            root_component: root_component.into(),
321            variables,
322        })
323    } else {
324        Err(errors.into())
325    }
326}
327
328fn collect_ir_vertices(root_component: &IRQueryComponent) -> BTreeMap<Vid, IRVertex> {
329    let mut result = Default::default();
330    collect_ir_vertices_recursive_step(&mut result, root_component);
331    result
332}
333
334fn collect_ir_vertices_recursive_step(
335    result: &mut BTreeMap<Vid, IRVertex>,
336    component: &IRQueryComponent,
337) {
338    result.extend(component.vertices.iter().map(|(k, v)| (*k, v.clone())));
339
340    component
341        .folds
342        .values()
343        .for_each(move |fold| collect_ir_vertices_recursive_step(result, &fold.component))
344}
345
346fn fill_in_query_variables(
347    variables: &mut BTreeMap<Arc<str>, Type>,
348    component: &IRQueryComponent,
349) -> Result<(), Vec<FilterTypeError>> {
350    let mut errors: Vec<FilterTypeError> = vec![];
351
352    let all_variable_uses = component
353        .vertices
354        .values()
355        .flat_map(|vertex| &vertex.filters)
356        .map(|filter| filter.right())
357        .chain(
358            component
359                .folds
360                .values()
361                .flat_map(|fold| &fold.post_filters)
362                .map(|filter| filter.right()),
363        )
364        .filter_map(|rhs| match rhs {
365            Some(Argument::Variable(vref)) => Some(vref),
366            _ => None,
367        });
368    for vref in all_variable_uses {
369        let existing_type = variables
370            .entry(vref.variable_name.clone())
371            .or_insert_with(|| vref.variable_type.clone());
372
373        match existing_type.intersect(&vref.variable_type) {
374            Some(intersection) => {
375                *existing_type = intersection;
376            }
377            None => {
378                errors.push(FilterTypeError::IncompatibleVariableTypeRequirements(
379                    vref.variable_name.to_string(),
380                    existing_type.to_string(),
381                    vref.variable_type.to_string(),
382                ));
383            }
384        }
385    }
386
387    for fold in component.folds.values() {
388        if let Err(e) = fill_in_query_variables(variables, fold.component.as_ref()) {
389            errors.extend(e);
390        }
391    }
392
393    if errors.is_empty() {
394        Ok(())
395    } else {
396        Err(errors)
397    }
398}
399
400fn make_duplicated_output_names_error(
401    ir_vertices: &BTreeMap<Vid, IRVertex>,
402    duplicates: BTreeMap<Arc<str>, Vec<FieldRef>>,
403) -> Vec<FrontendError> {
404    let conflict_info = DuplicatedNamesConflict {
405        duplicates: duplicates
406            .iter()
407            .map(|(k, fields)| {
408                let duplicate_values = fields
409                    .iter()
410                    .map(|field| match field {
411                        FieldRef::ContextField(field) => {
412                            let vid = field.vertex_id;
413                            (ir_vertices[&vid].type_name.to_string(), field.field_name.to_string())
414                        }
415                        FieldRef::FoldSpecificField(field) => {
416                            let vid = field.fold_root_vid;
417                            match field.kind {
418                                FoldSpecificFieldKind::Count => (
419                                    ir_vertices[&vid].type_name.to_string(),
420                                    "fold count value".to_string(),
421                                ),
422                            }
423                        }
424                    })
425                    .collect();
426                (k.to_string(), duplicate_values)
427            })
428            .collect(),
429    };
430    vec![FrontendError::MultipleOutputsWithSameName(conflict_info)]
431}
432
433#[allow(clippy::type_complexity)]
434fn check_for_duplicate_output_names(
435    maybe_duplicated_outputs: BTreeMap<Arc<str>, Vec<FieldRef>>,
436) -> Result<BTreeMap<Arc<str>, FieldRef>, BTreeMap<Arc<str>, Vec<FieldRef>>> {
437    maybe_duplicated_outputs
438        .into_iter()
439        .flat_map(|(name, outputs)| outputs.into_iter().map(move |o| (name.clone(), o)))
440        .try_collect_unique()
441}
442
443#[allow(clippy::too_many_arguments)]
444fn make_query_component<'schema, 'query, V, E>(
445    schema: &'schema Schema,
446    query: &'query Query,
447    vid_maker: &mut V,
448    eid_maker: &mut E,
449    component_path: &mut ComponentPath,
450    output_handler: &mut OutputHandler<'query>,
451    tags: &mut TagHandler<'query>,
452    parent_vid: Option<Vid>,
453    starting_vid: Vid,
454    pre_coercion_type: Arc<str>,
455    post_coercion_type: Arc<str>,
456    starting_field: &'query FieldNode,
457) -> Result<IRQueryComponent, Vec<FrontendError>>
458where
459    'schema: 'query,
460    V: Iterator<Item = Vid>,
461    E: Iterator<Item = Eid>,
462{
463    let mut errors: Vec<FrontendError> = vec![];
464
465    // Vid -> (vertex type, node that represents it)
466    let mut vertices: BTreeMap<Vid, (Arc<str>, &'query FieldNode)> = Default::default();
467
468    // Eid -> (from vid, to vid, connection that represents it)
469    let mut edges: BTreeMap<Eid, (Vid, Vid, &'query FieldConnection)> = Default::default();
470
471    // Vid -> vec of property names at that vertex used in the query
472    let mut property_names_by_vertex: BTreeMap<Vid, Vec<Arc<str>>> = Default::default();
473
474    // (Vid, property name) -> (property name, property type, nodes that represent the property)
475    #[allow(clippy::type_complexity)]
476    let mut properties: BTreeMap<
477        (Vid, Arc<str>),
478        (Arc<str>, Type, SmallVec<[&'query FieldNode; 1]>),
479    > = Default::default();
480
481    output_handler.begin_subcomponent();
482
483    let mut folds: BTreeMap<Eid, Arc<IRFold>> = Default::default();
484    if let Err(e) = fill_in_vertex_data(
485        schema,
486        query,
487        vid_maker,
488        eid_maker,
489        &mut vertices,
490        &mut edges,
491        &mut folds,
492        &mut property_names_by_vertex,
493        &mut properties,
494        component_path,
495        output_handler,
496        tags,
497        None,
498        starting_vid,
499        pre_coercion_type,
500        post_coercion_type,
501        starting_field,
502    ) {
503        errors.extend(e);
504    }
505
506    let vertex_results = vertices.iter().map(|(vid, (uncoerced_type_name, field_node))| {
507        make_vertex(
508            schema,
509            &property_names_by_vertex,
510            &properties,
511            tags,
512            component_path,
513            *vid,
514            uncoerced_type_name,
515            field_node,
516        )
517    });
518
519    let ir_vertices: BTreeMap<Vid, IRVertex> = vertex_results
520        .filter_map(|res| match res {
521            Ok(v) => Some((v.vid, v)),
522            Err(e) => {
523                errors.extend(e);
524                None
525            }
526        })
527        .try_collect_unique()
528        .unwrap();
529    if !errors.is_empty() {
530        return Err(errors);
531    }
532
533    let mut ir_edges: BTreeMap<Eid, Arc<IREdge>> = BTreeMap::new();
534    for (eid, (from_vid, to_vid, field_connection)) in edges.iter() {
535        let from_vertex_type = &ir_vertices[from_vid].type_name;
536        let edge_definition = get_edge_definition_from_schema(
537            schema,
538            from_vertex_type.as_ref(),
539            field_connection.name.as_ref(),
540        );
541        let edge_name = edge_definition.name.node.as_ref().to_owned().into();
542
543        let parameters_result = make_edge_parameters(edge_definition, &field_connection.arguments);
544
545        let optional = field_connection.optional.is_some();
546        let recursive = match field_connection.recurse.as_ref() {
547            None => None,
548            Some(d) => {
549                match get_recurse_implicit_coercion(
550                    schema,
551                    &ir_vertices[from_vid],
552                    edge_definition,
553                    d,
554                ) {
555                    Ok(coerce_to) => Some(Recursive::new(d.depth, coerce_to)),
556                    Err(e) => {
557                        errors.push(e);
558                        None
559                    }
560                }
561            }
562        };
563
564        match parameters_result {
565            Ok(parameters) => {
566                ir_edges.insert(
567                    *eid,
568                    IREdge {
569                        eid: *eid,
570                        from_vid: *from_vid,
571                        to_vid: *to_vid,
572                        edge_name,
573                        parameters,
574                        optional,
575                        recursive,
576                    }
577                    .into(),
578                );
579            }
580            Err(e) => {
581                errors.extend(e);
582            }
583        }
584    }
585
586    if !errors.is_empty() {
587        return Err(errors);
588    }
589
590    let maybe_duplicated_outputs = output_handler.end_subcomponent();
591
592    let component_outputs = match check_for_duplicate_output_names(maybe_duplicated_outputs) {
593        Ok(outputs) => outputs,
594        Err(duplicates) => {
595            return Err(make_duplicated_output_names_error(&ir_vertices, duplicates))
596        }
597    };
598
599    // TODO: fixme, temporary hack to avoid changing the IRQueryComponent struct
600    let hacked_outputs = component_outputs
601        .into_iter()
602        .filter_map(|(k, v)| match v {
603            FieldRef::ContextField(c) => Some((k, c)),
604            FieldRef::FoldSpecificField(_) => None,
605        })
606        .collect();
607
608    Ok(IRQueryComponent {
609        root: starting_vid,
610        vertices: ir_vertices,
611        edges: ir_edges,
612        folds,
613        outputs: hacked_outputs,
614    })
615}
616
617/// Four possible cases exist for the relationship between the `from_vid` vertex type
618/// and the destination type of the edge as defined on the field representing it.
619/// Let's the `from_vid` vertex type be S for "source,"
620/// let the recursed edge's field name be `e` for "edge,"
621/// and let the vertex type within the S.e field be called D for "destination.""
622/// 1. The two types S and D are completely unrelated.
623/// 2. S is a strict supertype of D.
624/// 3. S is equal to D.
625/// 4. S is a strict subtype of D.
626///
627/// Cases 1. and 2. return Err: recursion starts at depth = 0, so the `from_vid` vertex
628/// must be assigned to the D-typed scope within the S.e field, which is a type error
629/// due to the incompatible types of S and D.
630///
631/// Case 3 is Ok and is straightforward.
632///
633/// Case 4 may be Ok and may be Err, and if Ok it *may* require an implicit coercion.
634/// If D has a D.e field, two sub-cases are possible:
635/// 4a. D has a D.e field pointing to a vertex type of D. (Due to schema validity,
636///     D.e cannot point to a subtype of D since S.e must be an equal or narrower type
637///     than D.e.)
638///     This case is Ok and does not require an implicit coercion: the desired edge
639///     exists at all types encountered in the recursion.
640/// 4b. D has a D.e field, but it points to a vertex type that is a supertype of D.
641///     This would require another implicit coercion after expanding D.e
642///     (i.e. when recursing from depth = 2+) and may require more coercions
643///     deeper still since the depth = 1 point of view is analogous to case 4 as a whole.
644///     This case is currently not supported and will produce Err, but may become
645///     supported in the future.
646///     (Note that D.e cannot point to a completely unrelated type, since S is a subtype
647///      of D and therefore S.e must be an equal or narrower type than D.e or else
648///      the schema is not valid.)
649///
650/// If D does not have a D.e field, two more sub-cases are possible:
651/// 4c. D does not have a D.e field, but the S.e field has an unambiguous origin type:
652///     there's exactly one type X, subtype of D and either supertype of or equal to S,
653///     which defines X.e and from which S.e is derived. Again, due to schema validity,
654///     S.e must be an equal or narrower type than X.e, so the vertex type within X.e
655///     must be either equal to or a supertype of D, the vertex type of S.e.
656///     - If a supertype of D, this currently returns Err and is not supported because
657///       of the same reason as case 4b. It may be supported in the future.
658///     - If X.e has a vertex type equal to D, this returns Ok and requires
659///       an implicit coercion to X when recursing from depth = 1+.
660/// 4d. D does not have a D.e field, and the S.e field has an ambiguous origin type:
661///     there are at least two interfaces X and Y, where neither implements the other,
662///     such that S implements both of them, and both the X.e and Y.e fields are defined.
663///     In this case, it's not clear whether the implicit coercion should coerce
664///     to X or to Y, so this is an Err.
665fn get_recurse_implicit_coercion(
666    schema: &Schema,
667    from_vertex: &IRVertex,
668    edge_definition: &FieldDefinition,
669    d: &RecurseDirective,
670) -> Result<Option<Arc<str>>, FrontendError> {
671    let source_type = &from_vertex.type_name;
672    let destination_type = get_underlying_named_type(&edge_definition.ty.node).as_ref();
673
674    if !schema.is_named_type_subtype(destination_type, source_type) {
675        // Case 1 or 2, Err() in both cases.
676        // For the sake of a better error, we'll check which it is.
677        if !schema.is_named_type_subtype(source_type, destination_type) {
678            // Case 1, types are unrelated. Recursion on this edge is nonsensical.
679            return Err(FrontendError::RecursingNonRecursableEdge(
680                edge_definition.name.node.to_string(),
681                source_type.to_string(),
682                destination_type.to_string(),
683            ));
684        } else {
685            // Case 2, the destination type is a subtype of the source type.
686            // The vertex where the recursion starts might not "fit" in the depth = 0 recursion,
687            // but the user could explicitly use a type coercion to coerce the starting vertex
688            // into the destination type to make it work.
689            return Err(FrontendError::RecursionToSubtype(
690                edge_definition.name.node.to_string(),
691                source_type.to_string(),
692                destination_type.to_string(),
693            ));
694        }
695    }
696
697    if source_type.as_ref() == destination_type {
698        // Case 3, Ok() and no coercion required.
699        return Ok(None);
700    }
701
702    // Case 4, check whether the destination type also has an edge by that name.
703    let edge_name: Arc<str> = Arc::from(edge_definition.name.node.as_ref());
704    let destination_edge = schema.fields.get(&(Arc::from(destination_type), edge_name.clone()));
705    match destination_edge {
706        Some(destination_edge) => {
707            // The destination type has that edge too.
708            let edge_type = get_underlying_named_type(&destination_edge.ty.node).as_ref();
709            if edge_type == destination_type {
710                // Case 4a, Ok() and no coercion required.
711                Ok(None)
712            } else {
713                // Case 4b, Err() because it's not supported yet.
714                Err(FrontendError::EdgeRecursionNeedingMultipleCoercions(edge_name.to_string()))
715            }
716        }
717        None => {
718            // The destination type doesn't have that edge. Try to find a unique implicit coercion
719            // to a type that does have that edge so we can make the recursion work.
720            let edge_origin = &schema.field_origins[&(source_type.clone(), edge_name.clone())];
721            match edge_origin {
722                FieldOrigin::SingleAncestor(ancestor) => {
723                    // Case 4c, check the ancestor type's edge field type for two more sub-cases.
724                    let ancestor_edge = &schema.fields[&(ancestor.clone(), edge_name.clone())];
725                    let edge_type = get_underlying_named_type(&ancestor_edge.ty.node).as_ref();
726                    if edge_type == destination_type {
727                        // A single implicit coercion to the ancestor type will work here.
728                        Ok(Some(ancestor.clone()))
729                    } else {
730                        Err(FrontendError::EdgeRecursionNeedingMultipleCoercions(
731                            edge_name.to_string(),
732                        ))
733                    }
734                }
735                FieldOrigin::MultipleAncestors(multiple) => {
736                    // Case 4d, Err() because we can't figure out which implicit coercion to use.
737                    Err(FrontendError::AmbiguousOriginEdgeRecursion(edge_name.to_string()))
738                }
739            }
740        }
741    }
742}
743
744#[allow(clippy::too_many_arguments)]
745#[allow(clippy::type_complexity)]
746fn make_vertex<'query>(
747    schema: &Schema,
748    property_names_by_vertex: &BTreeMap<Vid, Vec<Arc<str>>>,
749    properties: &BTreeMap<(Vid, Arc<str>), (Arc<str>, Type, SmallVec<[&'query FieldNode; 1]>)>,
750    tags: &mut TagHandler<'_>,
751    component_path: &ComponentPath,
752    vid: Vid,
753    uncoerced_type_name: &Arc<str>,
754    field_node: &'query FieldNode,
755) -> Result<IRVertex, Vec<FrontendError>> {
756    let mut errors: Vec<FrontendError> = vec![];
757
758    // If the current vertex is the root of a `@fold`, then sometimes outputs are allowed.
759    // This will be handled and checked in the fold creation function, so ignore it here.
760    //
761    // If the current vertex is not the root of a fold, then outputs are not allowed
762    // and we should report an error.
763    let is_fold_root = component_path.is_component_root(vid);
764    if !is_fold_root && !field_node.output.is_empty() {
765        errors.push(FrontendError::UnsupportedEdgeOutput(field_node.name.as_ref().to_owned()));
766    }
767
768    if let Some(first_filter) = field_node.filter.first() {
769        // TODO: If @filter on edges is allowed, tweak this.
770        errors.push(FrontendError::UnsupportedEdgeFilter(field_node.name.as_ref().to_owned()));
771    }
772
773    if let Some(first_tag) = field_node.tag.first() {
774        // TODO: If @tag on edges is allowed, tweak this.
775        errors.push(FrontendError::UnsupportedEdgeTag(field_node.name.as_ref().to_owned()));
776    }
777
778    let default_func = || {
779        Result::<(Arc<str>, Option<Arc<str>>), FrontendError>::Ok((
780            uncoerced_type_name.clone(),
781            None,
782        ))
783    };
784    let mapper_func = |coerced_to_type: Arc<str>| {
785        let coerced_type =
786            get_vertex_type_definition_from_schema(schema, coerced_to_type.as_ref())?;
787        Ok((coerced_type.name.node.as_ref().to_owned().into(), Some(uncoerced_type_name.clone())))
788    };
789    let (type_name, coerced_from_type) =
790        match field_node.coerced_to.clone().map_or_else(default_func, mapper_func) {
791            Ok(x) => x,
792            Err(e) => {
793                errors.push(e);
794                return Err(errors);
795            }
796        };
797
798    let mut filters = vec![];
799    for property_name in property_names_by_vertex.get(&vid).into_iter().flatten() {
800        let (_, property_type, property_fields) =
801            properties.get(&(vid, property_name.clone())).unwrap();
802
803        for property_field in property_fields.iter() {
804            for filter_directive in property_field.filter.iter() {
805                match make_local_field_filter_expr(
806                    schema,
807                    component_path,
808                    tags,
809                    vid,
810                    property_name,
811                    property_type,
812                    filter_directive,
813                ) {
814                    Ok(filter_operation) => {
815                        filters.push(filter_operation);
816                    }
817                    Err(e) => {
818                        errors.extend(e);
819                    }
820                }
821            }
822        }
823    }
824
825    if errors.is_empty() {
826        Ok(IRVertex { vid, type_name, coerced_from_type, filters })
827    } else {
828        Err(errors)
829    }
830}
831
832#[allow(clippy::too_many_arguments)]
833#[allow(clippy::type_complexity)]
834fn fill_in_vertex_data<'schema, 'query, V, E>(
835    schema: &'schema Schema,
836    query: &'query Query,
837    vid_maker: &mut V,
838    eid_maker: &mut E,
839    vertices: &mut BTreeMap<Vid, (Arc<str>, &'query FieldNode)>,
840    edges: &mut BTreeMap<Eid, (Vid, Vid, &'query FieldConnection)>,
841    folds: &mut BTreeMap<Eid, Arc<IRFold>>,
842    property_names_by_vertex: &mut BTreeMap<Vid, Vec<Arc<str>>>,
843    properties: &mut BTreeMap<(Vid, Arc<str>), (Arc<str>, Type, SmallVec<[&'query FieldNode; 1]>)>,
844    component_path: &mut ComponentPath,
845    output_handler: &mut OutputHandler<'query>,
846    tags: &mut TagHandler<'query>,
847    parent_vid: Option<Vid>,
848    current_vid: Vid,
849    pre_coercion_type: Arc<str>,
850    post_coercion_type: Arc<str>,
851    current_field: &'query FieldNode,
852) -> Result<(), Vec<FrontendError>>
853where
854    'schema: 'query,
855    V: Iterator<Item = Vid>,
856    E: Iterator<Item = Eid>,
857{
858    let mut errors: Vec<FrontendError> = vec![];
859
860    vertices.insert_or_error(current_vid, (pre_coercion_type, current_field)).unwrap();
861
862    let defined_fields = get_vertex_field_definitions(schema, post_coercion_type.as_ref());
863
864    for (connection, subfield) in &current_field.connections {
865        let (
866            subfield_name,
867            subfield_pre_coercion_type,
868            subfield_post_coercion_type,
869            subfield_raw_type,
870        ) = get_field_name_and_type_from_schema(defined_fields, subfield);
871        if schema.vertex_types.contains_key(subfield_post_coercion_type.as_ref()) {
872            // Processing an edge.
873
874            let next_vid = vid_maker.next().unwrap();
875            let next_eid = eid_maker.next().unwrap();
876            output_handler
877                .begin_nested_scope(next_vid, subfield.alias.as_ref().map(|x| x.as_ref()));
878
879            if let Some(fold_group) = &connection.fold {
880                if connection.optional.is_some() {
881                    errors.push(FrontendError::UnsupportedDirectiveOnFoldedEdge(
882                        subfield.name.to_string(),
883                        "@optional".to_owned(),
884                    ));
885                }
886                if connection.recurse.is_some() {
887                    errors.push(FrontendError::UnsupportedDirectiveOnFoldedEdge(
888                        subfield.name.to_string(),
889                        "@recurse".to_owned(),
890                    ));
891                }
892
893                let edge_definition = get_edge_definition_from_schema(
894                    schema,
895                    post_coercion_type.as_ref(),
896                    connection.name.as_ref(),
897                );
898                match make_edge_parameters(edge_definition, &connection.arguments) {
899                    Ok(edge_parameters) => {
900                        match make_fold(
901                            schema,
902                            query,
903                            vid_maker,
904                            eid_maker,
905                            component_path,
906                            output_handler,
907                            tags,
908                            fold_group,
909                            next_eid,
910                            edge_definition.name.node.as_str().to_owned().into(),
911                            edge_parameters,
912                            current_vid,
913                            next_vid,
914                            subfield_pre_coercion_type,
915                            subfield_post_coercion_type,
916                            subfield,
917                        ) {
918                            Ok(fold) => {
919                                folds.insert(next_eid, fold.into());
920                            }
921                            Err(e) => {
922                                errors.extend(e);
923                            }
924                        }
925                    }
926                    Err(e) => {
927                        errors.extend(e);
928                    }
929                }
930            } else {
931                edges
932                    .insert_or_error(next_eid, (current_vid, next_vid, connection))
933                    .expect("Unexpectedly encountered duplicate eid");
934
935                if let Err(e) = fill_in_vertex_data(
936                    schema,
937                    query,
938                    vid_maker,
939                    eid_maker,
940                    vertices,
941                    edges,
942                    folds,
943                    property_names_by_vertex,
944                    properties,
945                    component_path,
946                    output_handler,
947                    tags,
948                    Some(current_vid),
949                    next_vid,
950                    subfield_pre_coercion_type.clone(),
951                    subfield_post_coercion_type.clone(),
952                    subfield,
953                ) {
954                    errors.extend(e);
955                }
956            }
957
958            output_handler.end_nested_scope(next_vid);
959        } else if get_builtin_scalars().contains(subfield_post_coercion_type.as_ref())
960            || schema.scalars.contains_key(subfield_post_coercion_type.as_ref())
961            || subfield_name == TYPENAME_META_FIELD
962        {
963            // Processing a property.
964
965            // @fold is not allowed on a property
966            if connection.fold.is_some() {
967                errors.push(FrontendError::UnsupportedDirectiveOnProperty(
968                    "@fold".into(),
969                    subfield.name.to_string(),
970                ));
971            }
972
973            // @optional is not allowed on a property
974            if connection.optional.is_some() {
975                errors.push(FrontendError::UnsupportedDirectiveOnProperty(
976                    "@optional".into(),
977                    subfield.name.to_string(),
978                ));
979            }
980
981            // @recurse is not allowed on a property
982            if connection.recurse.is_some() {
983                errors.push(FrontendError::UnsupportedDirectiveOnProperty(
984                    "@recurse".into(),
985                    subfield.name.to_string(),
986                ));
987            }
988
989            let subfield_name: Arc<str> = subfield_name.into();
990            let key = (current_vid, subfield_name.clone());
991            properties
992                .entry(key)
993                .and_modify(|(prior_name, prior_type, subfields)| {
994                    assert_eq!(subfield_name.as_ref(), prior_name.as_ref());
995                    assert_eq!(&subfield_raw_type, prior_type);
996                    subfields.push(subfield);
997                })
998                .or_insert_with(|| {
999                    property_names_by_vertex
1000                        .entry(current_vid)
1001                        .or_default()
1002                        .push(subfield_name.clone());
1003
1004                    (subfield_name, subfield_raw_type.clone(), SmallVec::from([subfield]))
1005                });
1006
1007            for output_directive in &subfield.output {
1008                // TODO: handle outputs of non-fold-related transformed fields here.
1009                let field_ref = FieldRef::ContextField(ContextField {
1010                    vertex_id: current_vid,
1011                    field_name: subfield.name.clone(),
1012                    field_type: subfield_raw_type.clone(),
1013                });
1014
1015                // The output's name can be either explicit or local (i.e. implicitly prefixed).
1016                // Explicit names are given explicitly in the directive:
1017                //     @output(name: "foo")
1018                // This would result in a "foo" output name, regardless of any prefixes.
1019                // Local names use the field's alias, if present, falling back to the field's name
1020                // otherwise. The local name is appended to any prefixes given as aliases
1021                // applied to the edges whose scopes enclose the output.
1022                if let Some(explicit_name) = output_directive.name.as_ref() {
1023                    output_handler
1024                        .register_explicitly_named_output(explicit_name.clone(), field_ref);
1025                } else {
1026                    let local_name = subfield
1027                        .alias
1028                        .as_ref()
1029                        .map(|x| x.as_ref())
1030                        .unwrap_or_else(|| subfield.name.as_ref());
1031                    output_handler.register_locally_named_output(local_name, None, field_ref);
1032                }
1033            }
1034
1035            for tag_directive in &subfield.tag {
1036                // The tag's name is the first of the following that is defined:
1037                // - the explicit "name" parameter in the @tag directive itself
1038                // - the alias of the field with the @tag directive
1039                // - the name of the field with the @tag directive
1040                let tag_name =
1041                    tag_directive.name.as_ref().map(|x| x.as_ref()).unwrap_or_else(|| {
1042                        subfield
1043                            .alias
1044                            .as_ref()
1045                            .map(|x| x.as_ref())
1046                            .unwrap_or_else(|| subfield.name.as_ref())
1047                    });
1048                let tag_field = ContextField {
1049                    vertex_id: current_vid,
1050                    field_name: subfield.name.clone(),
1051                    field_type: subfield_raw_type.clone(),
1052                };
1053
1054                // TODO: handle tags on non-fold-related transformed fields here
1055                if let Err(e) =
1056                    tags.register_tag(tag_name, FieldRef::ContextField(tag_field), component_path)
1057                {
1058                    errors.push(FrontendError::MultipleTagsWithSameName(tag_name.to_string()));
1059                }
1060            }
1061        } else {
1062            unreachable!("field name: {}", subfield_name);
1063        }
1064    }
1065
1066    if errors.is_empty() {
1067        Ok(())
1068    } else {
1069        Err(errors)
1070    }
1071}
1072
1073#[allow(clippy::too_many_arguments)]
1074fn make_fold<'schema, 'query, V, E>(
1075    schema: &'schema Schema,
1076    query: &'query Query,
1077    vid_maker: &mut V,
1078    eid_maker: &mut E,
1079    component_path: &mut ComponentPath,
1080    output_handler: &mut OutputHandler<'query>,
1081    tags: &mut TagHandler<'query>,
1082    fold_group: &'query FoldGroup,
1083    fold_eid: Eid,
1084    edge_name: Arc<str>,
1085    edge_parameters: EdgeParameters,
1086    parent_vid: Vid,
1087    starting_vid: Vid,
1088    starting_pre_coercion_type: Arc<str>,
1089    starting_post_coercion_type: Arc<str>,
1090    starting_field: &'query FieldNode,
1091) -> Result<IRFold, Vec<FrontendError>>
1092where
1093    'schema: 'query,
1094    V: Iterator<Item = Vid>,
1095    E: Iterator<Item = Eid>,
1096{
1097    component_path.push(starting_vid);
1098    tags.begin_subcomponent(starting_vid);
1099
1100    let mut errors = vec![];
1101    let component = make_query_component(
1102        schema,
1103        query,
1104        vid_maker,
1105        eid_maker,
1106        component_path,
1107        output_handler,
1108        tags,
1109        Some(parent_vid),
1110        starting_vid,
1111        starting_pre_coercion_type,
1112        starting_post_coercion_type,
1113        starting_field,
1114    )?;
1115    component_path.pop(starting_vid);
1116    let imported_tags = tags.end_subcomponent(starting_vid);
1117
1118    if !starting_field.output.is_empty() {
1119        // The edge has @fold @output but no @transform.
1120        // If it had a @transform then the output would have been in the field's transform group.
1121        errors.push(FrontendError::UnsupportedEdgeOutput(starting_field.name.as_ref().to_owned()));
1122    }
1123
1124    let mut post_filters = vec![];
1125    let mut fold_specific_outputs = BTreeMap::new();
1126
1127    if let Some(transform_group) = &fold_group.transform {
1128        if transform_group.retransform.is_some() {
1129            unimplemented!("re-transforming a @fold @transform value is currently not supported");
1130        }
1131
1132        let fold_specific_field = match transform_group.transform.kind {
1133            TransformationKind::Count => FoldSpecificField {
1134                fold_eid,
1135                fold_root_vid: starting_vid,
1136                kind: FoldSpecificFieldKind::Count,
1137            },
1138        };
1139        let field_ref = FieldRef::FoldSpecificField(fold_specific_field.clone());
1140
1141        for filter_directive in &transform_group.filter {
1142            match make_filter_expr(
1143                schema,
1144                component_path,
1145                tags,
1146                starting_vid,
1147                fold_specific_field.kind,
1148                filter_directive,
1149            ) {
1150                Ok(filter) => post_filters.push(filter),
1151                Err(e) => errors.extend(e),
1152            }
1153        }
1154        for output in &transform_group.output {
1155            let final_output_name = match output.name.as_ref() {
1156                Some(explicit_name) => {
1157                    output_handler
1158                        .register_explicitly_named_output(explicit_name.clone(), field_ref.clone());
1159                    explicit_name.clone()
1160                }
1161                None => {
1162                    let local_name = if starting_field.alias.is_some() {
1163                        // The field has an alias already, so don't bother adding the edge name
1164                        // to the output name.
1165                        ""
1166                    } else {
1167                        // The field does not have an alias, so use the edge name as the base
1168                        // of the name.
1169                        starting_field.name.as_ref()
1170                    };
1171                    output_handler.register_locally_named_output(
1172                        local_name,
1173                        Some(&[fold_specific_field.kind.transform_suffix()]),
1174                        field_ref.clone(),
1175                    )
1176                }
1177            };
1178
1179            let prior_output_by_that_name =
1180                fold_specific_outputs.insert(final_output_name.clone(), fold_specific_field.kind);
1181            if let Some(prior_output_kind) = prior_output_by_that_name {
1182                errors.push(FrontendError::MultipleOutputsWithSameName(DuplicatedNamesConflict {
1183                    duplicates: btreemap! {
1184                        final_output_name.to_string() => vec![
1185                            (starting_field.name.to_string(), prior_output_kind.field_name().to_string()),
1186                            (starting_field.name.to_string(), fold_specific_field.kind.field_name().to_string()),
1187                        ]
1188                    }
1189                }))
1190            }
1191        }
1192        for tag_directive in &transform_group.tag {
1193            let tag_name = tag_directive.name.as_ref().map(|x| x.as_ref());
1194            if let Some(tag_name) = tag_name {
1195                let field = FieldRef::FoldSpecificField(fold_specific_field.clone());
1196
1197                if let Err(e) = tags.register_tag(tag_name, field, component_path) {
1198                    errors.push(FrontendError::MultipleTagsWithSameName(tag_name.to_string()));
1199                }
1200            } else {
1201                errors.push(FrontendError::ExplicitTagNameRequired(
1202                    starting_field.name.as_ref().to_owned(),
1203                ))
1204            }
1205        }
1206    }
1207
1208    if !errors.is_empty() {
1209        return Err(errors);
1210    }
1211
1212    Ok(IRFold {
1213        eid: fold_eid,
1214        from_vid: parent_vid,
1215        to_vid: starting_vid,
1216        edge_name,
1217        parameters: edge_parameters,
1218        component: component.into(),
1219        imported_tags,
1220        post_filters,
1221        fold_specific_outputs,
1222    })
1223}
1224
1225#[cfg(test)]
1226mod tests {
1227    use std::{
1228        fs,
1229        path::{Path, PathBuf},
1230        sync::OnceLock,
1231    };
1232
1233    use trustfall_filetests_macros::parameterize;
1234
1235    use crate::{
1236        frontend::make_ir_for_query,
1237        schema::Schema,
1238        test_types::{TestIRQuery, TestIRQueryResult, TestParsedGraphQLQueryResult},
1239    };
1240
1241    static FILESYSTEM_SCHEMA: OnceLock<Schema> = OnceLock::new();
1242    static NUMBERS_SCHEMA: OnceLock<Schema> = OnceLock::new();
1243    static NULLABLES_SCHEMA: OnceLock<Schema> = OnceLock::new();
1244    static RECURSES_SCHEMA: OnceLock<Schema> = OnceLock::new();
1245
1246    fn get_filesystem_schema() -> &'static Schema {
1247        FILESYSTEM_SCHEMA.get_or_init(|| {
1248            Schema::parse(fs::read_to_string("test_data/schemas/filesystem.graphql").unwrap())
1249                .unwrap()
1250        })
1251    }
1252
1253    fn get_numbers_schema() -> &'static Schema {
1254        NUMBERS_SCHEMA.get_or_init(|| {
1255            Schema::parse(fs::read_to_string("test_data/schemas/numbers.graphql").unwrap()).unwrap()
1256        })
1257    }
1258
1259    fn get_nullables_schema() -> &'static Schema {
1260        NULLABLES_SCHEMA.get_or_init(|| {
1261            Schema::parse(fs::read_to_string("test_data/schemas/nullables.graphql").unwrap())
1262                .unwrap()
1263        })
1264    }
1265
1266    fn get_recurses_schema() -> &'static Schema {
1267        RECURSES_SCHEMA.get_or_init(|| {
1268            Schema::parse(fs::read_to_string("test_data/schemas/recurses.graphql").unwrap())
1269                .unwrap()
1270        })
1271    }
1272
1273    #[test]
1274    fn test_schemas_load_correctly() {
1275        // We want to merely touch the lazy-static variables so they get initialized.
1276        // If that succeeds, even very cursory checks will suffice.
1277        assert!(get_filesystem_schema().vertex_types.len() > 3);
1278        assert!(!get_numbers_schema().vertex_types.is_empty());
1279        assert!(!get_nullables_schema().vertex_types.is_empty());
1280        assert!(!get_recurses_schema().vertex_types.is_empty());
1281    }
1282
1283    #[parameterize("trustfall_core/test_data/tests/frontend_errors")]
1284    fn frontend_errors(base: &Path, stem: &str) {
1285        parameterizable_tester(base, stem, ".frontend-error.ron")
1286    }
1287
1288    #[parameterize("trustfall_core/test_data/tests/execution_errors")]
1289    fn execution_errors(base: &Path, stem: &str) {
1290        parameterizable_tester(base, stem, ".ir.ron")
1291    }
1292
1293    #[parameterize("trustfall_core/test_data/tests/valid_queries")]
1294    fn valid_queries(base: &Path, stem: &str) {
1295        parameterizable_tester(base, stem, ".ir.ron")
1296    }
1297
1298    fn parameterizable_tester(base: &Path, stem: &str, check_file_suffix: &str) {
1299        let mut input_path = PathBuf::from(base);
1300        input_path.push(format!("{stem}.graphql-parsed.ron"));
1301
1302        let input_data = fs::read_to_string(input_path).unwrap();
1303        let test_query: TestParsedGraphQLQueryResult = ron::from_str(&input_data).unwrap();
1304        if test_query.is_err() {
1305            return;
1306        }
1307        let test_query = test_query.unwrap();
1308
1309        let schema: &Schema = match test_query.schema_name.as_str() {
1310            "filesystem" => get_filesystem_schema(),
1311            "numbers" => get_numbers_schema(),
1312            "nullables" => get_nullables_schema(),
1313            "recurses" => get_recurses_schema(),
1314            _ => unimplemented!("unrecognized schema name: {:?}", test_query.schema_name),
1315        };
1316
1317        let mut check_path = PathBuf::from(base);
1318        check_path.push(format!("{stem}{check_file_suffix}"));
1319        let check_data = fs::read_to_string(check_path).unwrap();
1320
1321        let arguments = test_query.arguments;
1322        let constructed_test_item =
1323            make_ir_for_query(schema, &test_query.query).map(move |ir_query| TestIRQuery {
1324                schema_name: test_query.schema_name,
1325                ir_query,
1326                arguments,
1327            });
1328
1329        let check_parsed: TestIRQueryResult = ron::from_str(&check_data).unwrap();
1330
1331        assert_eq!(check_parsed, constructed_test_item);
1332    }
1333}