solverforge-macros 0.19.0

Derive macros for SolverForge constraint solver
Documentation
struct GeneratedListShadowUpdate {
    update: TokenStream,
    reset: TokenStream,
    descriptor: Option<(usize, Ident)>,
}

fn generate_shadow_methods(model: &ModelMetadata) -> Result<TokenStream> {
    let solution_module = &model.solution.module_ident;
    let solution_ident = &model.solution.ident;
    let solution_path = quote! { #solution_module::#solution_ident };
    let generated = generate_list_shadow_update(model, &solution_path)?;
    let list_update = generated.update;
    let list_reset = generated.reset;

    let Some((descriptor_index, collection_accessor)) = generated.descriptor else {
        return Ok(quote! {
            fn update_entity_shadows(
                _solution: &mut Self,
                _descriptor_index: usize,
                _entity_index: usize,
            ) -> bool {
                false
            }

            fn update_all_shadows(_solution: &mut Self) -> bool {
                false
            }
        });
    };

    Ok(quote! {
        fn update_entity_shadows(
            solution: &mut Self,
            descriptor_index: usize,
            entity_index: usize,
        ) -> bool {
            let mut updated = false;
            #list_update
            updated
        }

        fn update_all_shadows(solution: &mut Self) -> bool {
            #list_reset
            for entity_index in 0..#solution_path::#collection_accessor(solution).len() {
                let _ = <Self as ::solverforge::__internal::PlanningModelSupport>::update_entity_shadows(
                    solution,
                    #descriptor_index,
                    entity_index,
                );
            }
            true
        }
    })
}

fn generate_list_shadow_update(
    model: &ModelMetadata,
    solution_path: &TokenStream,
) -> Result<GeneratedListShadowUpdate> {
    let config = &model.solution.shadow_config;
    if !list_shadow_updates_requested(config) {
        return Ok(GeneratedListShadowUpdate {
            update: TokenStream::new(),
            reset: TokenStream::new(),
            descriptor: None,
        });
    }

    let list_owner = config.list_owner.as_deref().ok_or_else(|| {
        Error::new(
            proc_macro2::Span::call_site(),
            "#[shadow_variable_updates(...)] requires `list_owner = \"entity_collection_field\"` when list shadow updates are configured",
        )
    })?;
    let owner_collection = model
        .solution
        .collections
        .iter()
        .find(|collection| {
            collection.field_name == list_owner && collection.descriptor_index.is_some()
        })
        .ok_or_else(|| {
            Error::new(
                proc_macro2::Span::call_site(),
                format!(
                    "#[shadow_variable_updates(list_owner = \"{list_owner}\")] must name a #[planning_entity_collection] field",
                ),
            )
        })?;
    let owner_ident = &owner_collection.field_ident;
    let owner_accessor = format_ident!("__solverforge_collection_{}", owner_ident);
    let owner_mut_accessor = format_ident!("__solverforge_collection_{}_mut", owner_ident);
    let descriptor_index = owner_collection.descriptor_index.unwrap();
    let entity_type_name = canonical_type_name(&model.aliases, &owner_collection.type_name);
    let entity = model
        .entities
        .get(entity_type_name)
        .expect("list owner entity should have been validated");
    let element_collection_name = entity.list_element_collection.as_deref().ok_or_else(|| {
        Error::new(
            proc_macro2::Span::call_site(),
            format!("list owner `{list_owner}` does not declare #[planning_list_variable]"),
        )
    })?;
    let list_variable_ident = entity
        .list_variable_name
        .as_deref()
        .map(|name| Ident::new(name, proc_macro2::Span::call_site()))
        .ok_or_else(|| {
            Error::new(
                proc_macro2::Span::call_site(),
                format!("list owner `{list_owner}` does not declare #[planning_list_variable]"),
            )
        })?;
    let element_collection = model
        .solution
        .collections
        .iter()
        .find(|collection| collection.field_name == element_collection_name)
        .ok_or_else(|| {
            Error::new(
                proc_macro2::Span::call_site(),
                format!(
                    "planning solution with list owner `{list_owner}` requires a collection field named `{element_collection_name}`",
                ),
            )
        })?;
    let element_accessor =
        format_ident!("__solverforge_collection_{}", element_collection.field_ident);
    let element_mut_accessor =
        format_ident!("__solverforge_collection_{}_mut", element_collection.field_ident);

    let inverse_update = config.inverse_field.as_ref().map(|field| {
        let field_ident = Ident::new(field, proc_macro2::Span::call_site());
        quote! {
            {
                let elements = #solution_path::#element_mut_accessor(solution);
                for &element_idx in &element_indices {
                    elements[element_idx].#field_ident = Some(entity_index);
                }
            }
        }
    });
    let index_update = config.index_field.as_ref().map(|field| {
        let field_ident = Ident::new(field, proc_macro2::Span::call_site());
        quote! {
            {
                let elements = #solution_path::#element_mut_accessor(solution);
                for (offset, &element_idx) in element_indices.iter().enumerate() {
                    elements[element_idx].#field_ident = Some(offset);
                }
            }
        }
    });
    let index_reset = config.index_field.as_ref().map(|field| {
        let field_ident = Ident::new(field, proc_macro2::Span::call_site());
        quote! {
            {
                let elements = #solution_path::#element_mut_accessor(solution);
                for element in elements {
                    element.#field_ident = None;
                }
            }
        }
    });
    let previous_update = config.previous_field.as_ref().map(|field| {
        let field_ident = Ident::new(field, proc_macro2::Span::call_site());
        quote! {
            {
                let elements = #solution_path::#element_mut_accessor(solution);
                let mut previous = None;
                for &element_idx in &element_indices {
                    elements[element_idx].#field_ident = previous;
                    previous = Some(element_idx);
                }
            }
        }
    });
    let next_update = config.next_field.as_ref().map(|field| {
        let field_ident = Ident::new(field, proc_macro2::Span::call_site());
        quote! {
            {
                let elements = #solution_path::#element_mut_accessor(solution);
                for (offset, &element_idx) in element_indices.iter().enumerate() {
                    elements[element_idx].#field_ident = element_indices.get(offset + 1).copied();
                }
            }
        }
    });
    let cascading_update = config.cascading_listener.as_ref().map(|method| {
        let method_ident = Ident::new(method, proc_macro2::Span::call_site());
        quote! {
            for &element_idx in &element_indices {
                solution.#method_ident(element_idx);
            }
        }
    });
    let post_update = config.post_update_listener.as_ref().map(|method| {
        let method_ident = Ident::new(method, proc_macro2::Span::call_site());
        quote! { solution.#method_ident(entity_index); }
    });
    let aggregate_updates = generate_list_aggregate_updates(
        config,
        solution_path,
        &element_accessor,
        &owner_mut_accessor,
    );
    let compute_updates = generate_list_compute_updates(config, solution_path, &owner_mut_accessor);

    let update = quote! {
        if descriptor_index == #descriptor_index
            && entity_index < #solution_path::#owner_accessor(solution).len()
        {
            let element_indices = #solution_path::#owner_accessor(solution)[entity_index]
                .#list_variable_ident
                .to_vec();
            #inverse_update
            #index_update
            #previous_update
            #next_update
            #cascading_update
            #(#aggregate_updates)*
            #(#compute_updates)*
            #post_update
            updated = true;
        }
    };
    Ok(GeneratedListShadowUpdate {
        update,
        reset: index_reset.unwrap_or_default(),
        descriptor: Some((descriptor_index, owner_accessor)),
    })
}

fn generate_list_aggregate_updates(
    config: &ShadowConfig,
    solution_path: &TokenStream,
    element_accessor: &Ident,
    owner_mut_accessor: &Ident,
) -> Vec<TokenStream> {
    config
        .entity_aggregates
        .iter()
        .filter_map(|spec| {
            let parts = spec.split(':').collect::<Vec<_>>();
            if parts.len() != 3 || parts[1] != "sum" {
                return None;
            }
            let target = Ident::new(parts[0], proc_macro2::Span::call_site());
            let source = Ident::new(parts[2], proc_macro2::Span::call_site());
            Some(quote! {
                let aggregate_value = {
                    let elements = #solution_path::#element_accessor(solution);
                    element_indices.iter().map(|&index| elements[index].#source).sum()
                };
                #solution_path::#owner_mut_accessor(solution)[entity_index].#target = aggregate_value;
            })
        })
        .collect()
}

fn generate_list_compute_updates(
    config: &ShadowConfig,
    solution_path: &TokenStream,
    owner_mut_accessor: &Ident,
) -> Vec<TokenStream> {
    config
        .entity_computes
        .iter()
        .filter_map(|spec| {
            let parts = spec.split(':').collect::<Vec<_>>();
            if parts.len() != 2 {
                return None;
            }
            let target = Ident::new(parts[0], proc_macro2::Span::call_site());
            let method = Ident::new(parts[1], proc_macro2::Span::call_site());
            Some(quote! {
                let computed_value = solution.#method(entity_index);
                #solution_path::#owner_mut_accessor(solution)[entity_index].#target = computed_value;
            })
        })
        .collect()
}

fn list_shadow_updates_requested(config: &ShadowConfig) -> bool {
    config.inverse_field.is_some()
        || config.index_field.is_some()
        || config.previous_field.is_some()
        || config.next_field.is_some()
        || config.cascading_listener.is_some()
        || config.post_update_listener.is_some()
        || !config.entity_aggregates.is_empty()
        || !config.entity_computes.is_empty()
}