# solverforge-macros WIREFRAME
Proc-macro crate providing attribute macros and derive macros for SolverForge domain model structs.
**Location:** `crates/solverforge-macros/`
**Workspace Release:** `0.19.4`
## Dependencies
- `syn` (workspace) — Rust syntax parsing
- `quote` (workspace) — Rust code generation
- `proc-macro2` (workspace) — Proc-macro token streams
- `solverforge` (dev-dependency) — Trybuild pass/fail fixtures
- `trybuild` (dev-dependency) — Compile-pass and compile-fail UI tests
No runtime solverforge crate dependencies. Generated code references
`::solverforge::__internal::*` and `::solverforge::*` at the call site.
## File Map
```
src/
├── attr_parse.rs — Shared attribute parsing helpers
├── attr_validation.rs — Strict user-authored attribute argument contracts and diagnostics
├── attr_validation_tests.rs — Attribute validation unit tests
├── constraints.rs — `#[solverforge_constraints]` compiler module root
├── constraints/*.rs — Constraint function AST, parse, plan, emit, and tests
├── entrypoints.rs — Shared proc-macro wrapper logic used by the crate root
├── lib.rs — Crate root; required proc-macro entry points only
├── planning_model.rs — `planning_model!` manifest parser, file reader, metadata validator, and model-support generator
├── planning_model/*.rs — Manifest parsing, module loading, metadata validation, scalar-group support generation, shadow generation, and tests split by responsibility
├── planning_entity.rs — planning-entity support derive module root
├── planning_entity/*.rs — Entity derive expansion, scalar/list-variable helpers, and utilities
├── planning_entity/expand/*.rs — Entity derive expansion and validation helpers
├── planning_entity_tests.rs — PlanningEntity derive tests
├── planning_solution.rs — planning-solution support derive module root
├── planning_solution/*.rs — Solution derive expansion, config/shadow/runtime/list roots, stream extensions, and type helpers
├── planning_solution/list_operations/*.rs — Private owner-specific list runtime helper generation
├── planning_solution/runtime/*.rs — Runtime module root, solve generation, scalar setup, and helper declarations
├── planning_solution/runtime/helpers/*.rs — Runtime helper code split by candidate counts, search declaration/support, and generated Solvable/runtime trait impls
├── planning_solution_tests.rs — PlanningSolution derive tests
└── problem_fact.rs — problem-fact support derive: ProblemFact, PlanningId, problem_fact_descriptor()
```
## Function-like Macros
### `planning_model!`
Canonical domain manifest for a SolverForge model. It is normally declared in
`src/domain/mod.rs`:
```rust
solverforge::planning_model! {
root = "src/domain";
mod item;
mod container;
mod plan;
pub use item::Item;
pub use container::Container;
pub use plan::Plan;
}
```
The macro accepts `root = "..."`, file-backed `mod name;` declarations, and
public `pub use ...;` exports. Public `type Alias = Type;` declarations and
public `pub use module::Type as Alias;` exports are resolved to the canonical
descriptor type name during validation. It reads the listed module files, emits
the same Rust modules and exports plus `include_str!` dependencies for every
read module, enforces exactly one `#[planning_solution]`, validates entity and
fact collection types, validates list element collection references, and
generates the hidden `PlanningModelSupport` impl used by the solution derive for
scalar hooks, runtime scalar and list slots, model validation, and configured
list-shadow updates.
## Attribute Macros (proc_macro_attribute)
### `#[planning_entity]` / `#[planning_entity(serde)]`
Applies to structs. Adds ordinary Rust derives plus hidden SolverForge support derive output. Optionally adds `serde::Serialize, serde::Deserialize` when `serde` flag is present.
### `#[planning_solution]` / `#[planning_solution(...)]`
Applies to structs. Adds ordinary Rust derives plus hidden SolverForge support
derive output. Accepted arguments are `serde`, `constraints = "path"`,
`config = "path"`, `solver_toml = "path"`, `search = "path"`,
`conflict_repairs = "path"`, and `scalar_groups = "path"`. Unknown or malformed
arguments are compile errors.
The `constraints` flag embeds a `#[solverforge_constraints_path = "path"]`
attribute for the derive to consume. The `config` flag embeds a
`#[solverforge_config_path = "path"]` attribute for the derive to consume; the
callback must have signature `fn(&Solution, SolverConfig) -> SolverConfig` and
decorates the loaded `solver.toml` config instead of replacing it.
### `#[problem_fact]` / `#[problem_fact(serde)]`
Applies to structs. Adds ordinary Rust derives plus hidden SolverForge support derive output. Optionally adds serde derives.
### `#[solverforge_constraints]`
Applies to a constraint factory function. The function remains normal fluent
Rust, but the macro parses the whole body before type checking so repeated
same-binding grouped stream terminals can become one shared incremental node
with multiple terminal scorers. Accepted sharing shape:
- A `let`-bound grouped, projected grouped, direct cross grouped, or
complemented grouped stream whose final tuple contains multiple
`.penalize(...).named("...")` or `.reward(...).named("...")` terminal calls on
that same binding.
The macro preserves terminal order, names, impact direction, and hard metadata.
Supported shared terminals are emitted back through the ordinary fluent
terminal path as a single chained stream finalization; the macro does not call
parallel shared-node constructors. Tuple members that are not part of the
repeated same-binding group are preserved and combined with the shared terminal
set through monomorphized `ConstraintSet` composition; the macro does not guess
whether an opaque expression is a singleton constraint or a multi-constraint set
from its syntax. Opaque tuples without a
repeated same-binding grouped terminal stay on the existing Rust path; the
compiler never adds a public `share`, `derive`, prefix, or suffix API.
Internal module responsibilities:
| Module | Responsibility |
|--------|----------------|
| `constraints/ast.rs` | Compiler-internal nodes, terminal constraints, impact kinds, and final program shape |
| `constraints/parse.rs` | Parses supported fluent terminals, `let` stream bindings, tuple tails, and `.named(...)` requirements |
| `constraints/plan.rs` | Selects shared grouped plans by repeated binding while preserving terminal order |
| `constraints/emit.rs` | Emits concrete shared-node code against `::solverforge::__internal::*` helpers |
## Derive Macros (proc_macro_derive)
### Planning Entity Support Derive
**Consumed attributes on fields:**
- `#[planning_id]` — marks the unique ID field
- `#[planning_variable(allows_unassigned = bool, value_range_provider = "name")]` — genuine planning variable on an `Option<usize>` field. Scalar values are candidate indexes into the declared value range.
canonical scalar candidate and nearby hooks are declared here as well:
`candidate_values = "fn_name"`, `nearby_value_candidates = "fn_name"`,
`nearby_entity_candidates = "fn_name"`,
`nearby_value_distance_meter = "fn_name"` and `nearby_entity_distance_meter = "fn_name"`
scalar construction ordering hooks are declared here too:
`construction_entity_order_key = "fn_name"` and
`construction_value_order_key = "fn_name"`. These are emitted for
construction routing and are not local-search selector ordering hooks.
- `#[planning_list_variable(...)]` — list planning variable
currently requires `Vec<usize>` and `element_collection = "solution_field"`;
optional `element_owner_fn = "path"` declares partial fixed ownership; the
concrete hook is called from the generated `PlanningModelSupport` impl and
adapted to the runtime list-slot owner function. Optional
`solution_trait = "path"` adds the solution trait bound used by custom list
helper hooks; `domain = "cvrp"` supplies the stock `VrpSolution` bound.
Optional
`construction_element_order_key = "path"` declares a plain
`fn(&Solution, element) -> i64` hook used by list construction through
existing runtime list-slot metadata. Optional
`precedence_duration_fn = "path"` and `precedence_successors_fn = "path"`
declare plain `fn(&Solution, element) -> usize` and
`fn(&Solution, element, &mut Vec<usize>)` hooks for stock list-precedence
scoring/selectors.
- `#[planning_pin]` — boolean field controlling entity pinning
- `#[inverse_relation_shadow_variable(source_variable_name = "field")]` — inverse relation shadow
- `#[index_shadow_variable(source_variable_name = "field")]` — list index shadow
- `#[previous_element_shadow_variable(source_variable_name = "field")]` — previous element shadow
- `#[next_element_shadow_variable(source_variable_name = "field")]` — next element shadow
- `#[cascading_update_shadow_variable]` — cascading update shadow
Unknown or malformed user-authored field arguments are compile errors.
Non-`Option<usize>` scalar planning variables are compile errors because the
runtime scalar surface is candidate-index based.
**Generated code:**
- `impl PlanningEntity for T` — `is_pinned()`, `as_any()`, `as_any_mut()`
- `impl PlanningId for T` (if `#[planning_id]` present) — `type Id` set to field type, `planning_id()` returns field value
- `impl T { pub fn entity_descriptor(solution_field: &'static str) -> EntityDescriptor }` — builds descriptor with all variable descriptors (genuine, list, shadow) and preserves `#[planning_id]` / `#[planning_pin]` metadata
- Hidden scalar metadata bridge: private indexed helpers for scalar variable count, name, allows-unassigned, value-source metadata, getter/setter, and entity-local value slices. Helper order matches `entity_descriptor()` genuine scalar variable order; the index is used for generated getter/setter dispatch, while manifest hook attachment resolves descriptor variables by descriptor index plus variable name.
- Hidden list metadata bridge (when the entity has a `#[planning_list_variable]` field): public cross-module `__SOLVERFORGE_LIST_VARIABLE_COUNT` plus private `__SOLVERFORGE_LIST_VARIABLE_NAME`, `__SOLVERFORGE_LIST_ELEMENT_COLLECTION`, `__solverforge_list_field()`, `__solverforge_list_field_mut()`, `__solverforge_list_metadata()`
- Hidden list metadata bridge implementation (when the entity has a `#[planning_list_variable]` field): `impl __internal::ListVariableEntity<Solution> for Entity`
- Hidden unassigned bridge (when the entity has exactly one `Option<_>` planning variable): `impl __internal::UnassignedEntity<Solution> for Entity`, enabling `.unassigned()` on `UniConstraintStream<_, Entity, ...>` without a generated public trait import
### Planning Solution Support Derive
**Consumed attributes on fields:**
- `#[planning_entity_collection]` — `Vec<Entity>` field containing planning entities
- `#[planning_list_element_collection(owner = "field")]` — `Vec<usize>` field containing all elements for the named list owner; optional when the solution has a matching `#[planning_entity_collection]` or `#[problem_fact_collection]` field whose name matches the entity list variable's `element_collection`
- `#[problem_fact_collection]` — `Vec<Fact>` field containing problem facts
- `#[planning_score]` — `Option<Score>` field (required)
- `#[value_range_provider]` — value range source
Collection marker attributes and `#[planning_score]` do not accept arguments.
`#[planning_list_element_collection]` accepts only `owner = "field"` when
arguments are present. Unknown or malformed user-authored solution field
arguments are compile errors.
**Consumed attributes on struct:**
- `#[shadow_variable_updates(...)]` — configures descriptor-aware shadow updates for the canonical solver path
- `#[solverforge_constraints_path = "path"]` — path to constraint factory function
- `#[solverforge_config_path = "path"]` — path to a config callback with signature `fn(&Solution, SolverConfig) -> SolverConfig`; called with the loaded `solver.toml` config (or defaults if the file is missing)
- `#[solverforge_solver_toml_path = "path"]` — generated bridge attribute for an explicit solver TOML source
- `#[solverforge_search_path = "path"]` — generated bridge attribute for a typed custom-search function consumed by runtime phase support
- `#[solverforge_conflict_repairs_path = "path"]` — generated bridge attribute for the current conflict-repair provider function
- `#[solverforge_scalar_groups_path = "path"]` — generated bridge attribute for scalar group declarations
**`#[shadow_variable_updates]` parameters:**
- `list_owner = "field"` — selects the `#[planning_entity_collection]` field whose entity owns the list shadow updates
- `inverse_field = "field"` — field on element for inverse mapping
- `index_field = "field"` — `Option<usize>` field on element for its current list position
- `previous_field = "field"` — field on element for previous pointer
- `next_field = "field"` — field on element for next pointer
- `cascading_listener = "method"` — method name for cascading updates per element
- `post_update_listener = "method"` — method name called after all shadow updates per entity
- `entity_aggregate = "target:sum:source"` — aggregate element field onto entity (sum only)
- `entity_compute = "target:method"` — compute entity field via method
**`#[planning_list_variable]` parameters:**
- `element_collection = "field"` — solution field with all list elements
- `domain = "cvrp"` — stock CVRP list-variable profile; supplies the CVRP solution trait, distance meters, strict route hooks with safe unreachable-leg rejection, relaxed Clarke-Wright savings hooks, and savings metric class
- `distance_meter = "path"` — optional cross-entity distance meter type
- `intra_distance_meter = "path"` — optional intra-entity distance meter type
- `route_hooks = "path"` — optional route-local hook module with `get`, `set`, `depot`, `distance`, and `feasible`; k-opt uses these hooks and Clarke-Wright uses only `set` for assignment
- `savings_hooks = "path"` — optional Clarke-Wright construction hook module with `depot`, `distance`, and `feasible`
- `savings_metric_class_fn = "path"` — optional Clarke-Wright metric-class hook, `fn(&Solution, entity_idx) -> usize`; owners in the same class must share construction depot and distance behavior
- `solution_trait = "path"` — optional solution trait bound required by custom list helper hooks; `domain = "cvrp"` supplies `::solverforge::cvrp::VrpSolution`
- `element_owner_fn = "path"` — optional partial fixed-owner hook, `fn(&Solution, element) -> Option<usize>`; `None` leaves that element unrestricted
- `construction_element_order_key = "path"` — optional list-construction ordering hook, `fn(&Solution, element) -> i64`; it flows through existing generated list metadata and runtime list slots
- `precedence_duration_fn = "path"` — optional precedence duration hook, `fn(&Solution, element) -> usize`; paired with `precedence_successors_fn`
- `precedence_successors_fn = "path"` — optional fixed-precedence successor hook, `fn(&Solution, element, &mut Vec<usize>)`; paired with `precedence_duration_fn`
When `domain = "cvrp"` is present, the profiled defaults above must be used as a
coherent stock bundle. Overriding `distance_meter`, `intra_distance_meter`,
`solution_trait`, `route_hooks`, `savings_hooks`, or `savings_metric_class_fn`
is rejected unless the override repeats the exact stock path. Custom routing
semantics should omit `domain = "cvrp"` and declare explicit hook paths instead.
**Generated code:**
- `impl PlanningSolution for T` — `type Score`, `score()`, `set_score()`, plus `update_entity_shadows()` / `update_all_shadows()` delegation to the manifest-owned support implementation when `#[shadow_variable_updates(...)]` configures list shadows.
- `impl T { pub fn descriptor() -> SolutionDescriptor }` — builds full descriptor with entity extractors and fact extractors, reusing entity-generated descriptors so field-level variable order and metadata are preserved
- `impl T { pub fn entity_count(&Self, descriptor_index: usize) -> usize }` — entity count by descriptor index
- Private owner-specific list operations used by the canonical runtime: `__solverforge_list_len_<owner>()`, `__solverforge_list_remove_<owner>()`, `__solverforge_list_insert_<owner>()`, `__solverforge_list_get_<owner>()`, `__solverforge_list_set_<owner>()`, `__solverforge_list_reverse_<owner>()`, `__solverforge_sublist_remove_<owner>()`, `__solverforge_sublist_insert_<owner>()`, `__solverforge_ruin_remove_<owner>()`, `__solverforge_ruin_insert_<owner>()`, `__solverforge_list_remove_for_construction_<owner>()`, `__solverforge_index_to_element_<owner>()`, `__solverforge_element_source_key_<owner>()`, `__solverforge_element_count_<owner>()`, `__solverforge_assigned_elements_<owner>()`, `__solverforge_n_entities_<owner>()`, `__solverforge_assign_element_<owner>()`, plus aggregate helpers `__solverforge_total_list_entities()` and `__solverforge_total_list_elements()`
- `impl SolvableSolution for T` — delegates to `descriptor()` and `entity_count()`
- `impl Solvable for T` (when constraints path specified) — `solve(self, runtime: SolverRuntime<Self>, qualified_candidate_trace_provenance: Option<QualifiedCandidateTraceRunProvenance>)` delegates to `solve_internal()`
- `impl Analyzable for T` (when constraints path specified) — `analyze()` creates `ScoreDirector` with canonical shadow support and returns `ScoreAnalysis`
- `fn solve_internal(self, runtime: SolverRuntime<Self>, qualified_candidate_trace_provenance: Option<QualifiedCandidateTraceRunProvenance>)` (when constraints path specified) — loads the configured base `SolverConfig` (or embedded `solver.toml`), applies the optional `config = "..."` callback, and calls hidden `try_run_solver_with_config_and_search`. The generated `__solverforge_search_declaration(config, descriptor)` builds one `RuntimeModel` containing scalar slots plus zero or more owner-specific list slots, delegates scalar hook and scalar-group attachment to the `planning_model!` support impl, sorts those variable slots to the descriptor-backed variable order emitted by the macros, constructs `SearchContext` with `try_new`, and returns the selected custom `Search` declaration or canonical defaults. The compiled runtime owns phase preparation, source binding, construction, local search, and terminal lifecycle; no macro-generated phase sequence exists.
- Public solution source methods for all `#[planning_entity_collection]`, `#[problem_fact_collection]`, and streamable `#[planning_list_element_collection]` fields. Each method is inherent on the solution type, for example `Plan::tasks()`, returns the concrete hidden `SourceExtract<fn(&Plan) -> &[Task]>` wrapper, and carries hidden `ChangeSource::Descriptor(idx)` for planning entities or `ChangeSource::Static` for facts and list elements. User constraints can call the explicit form `ConstraintFactory::new().for_each(Plan::tasks())` or import the generated `PlanConstraintStreams` trait and call `ConstraintFactory::new().tasks()`. Both paths use the same source metadata and concrete stream types.
### Problem Fact Support Derive
**Consumed attributes on fields:**
- `#[planning_id]` — marks the unique ID field
**Generated code:**
- `impl ProblemFact for T` — `as_any()`
- `impl PlanningId for T` (if `#[planning_id]` present) — same as entity version
- `impl T { pub fn problem_fact_descriptor(solution_field: &'static str) -> ProblemFactDescriptor }`
## Shared Helper Functions (`entrypoints.rs` / `attr_parse.rs` / `attr_validation.rs`, private)
| Function | Signature | Note |
|----------|-----------|------|
| `parse_serde_flag` | `fn(TokenStream, &str) -> Result<bool>` | Strictly parses top-level `serde` flags |
| `parse_solution_flags` | `fn(TokenStream) -> Result<SolutionFlags>` | Strictly parses current `#[planning_solution(...)]` arguments |
| `has_attribute` | `fn(&[Attribute], &str) -> bool` | Checks if field has named attribute |
| `get_attribute` | `fn(&[Attribute], &str) -> Option<&Attribute>` | Gets named attribute |
| `parse_attribute_bool` | `fn(&Attribute, &str) -> Option<bool>` | Parses `key = true/false` from attribute |
| `parse_attribute_string` | `fn(&Attribute, &str) -> Option<String>` | Parses `key = "value"` from attribute |
| `parse_attribute_list` | `fn(&Attribute, &str) -> Vec<String>` | Collects all `key = "value"` pairs for same key |
## Internal Helper Functions (`planning_solution/*.rs`, private)
| Function | Signature | Note |
|----------|-----------|------|
| `parse_constraints_path` | `fn(&[Attribute]) -> Option<String>` | Extracts `#[solverforge_constraints_path = "..."]` |
| `parse_config_path` | `fn(&[Attribute]) -> Option<String>` | Extracts `#[solverforge_config_path = "..."]` |
| `parse_solver_toml_path` | `fn(&[Attribute]) -> Option<String>` | Extracts `#[solverforge_solver_toml_path = "..."]` |
| `parse_search_path` | `fn(&[Attribute]) -> Option<String>` | Extracts `#[solverforge_search_path = "..."]` |
| `parse_conflict_repairs_path` | `fn(&[Attribute]) -> Option<String>` | Extracts `#[solverforge_conflict_repairs_path = "..."]` |
| `parse_scalar_groups_path` | `fn(&[Attribute]) -> Option<String>` | Extracts `#[solverforge_scalar_groups_path = "..."]` |
| `parse_shadow_config` | `fn(&[Attribute]) -> ShadowConfig` | Parses `#[shadow_variable_updates(...)]` |
| `generate_runtime_phase_support` | `fn(&Fields, &Option<String>, &Option<String>, &Option<String>, &Option<String>, &Ident) -> TokenStream` | Generates runtime-model assembly and the configured/custom search declaration for constraints, repairs, scalar groups, and search callbacks |
| `shadow_updates_requested` | `fn(&ShadowConfig) -> bool` | Detects whether real shadow update work is configured |
| `generate_list_operations` | `fn(&Fields) -> TokenStream` | Generates private owner-specific list mutation/source helpers and aggregate list entity/element counts for the canonical runtime |
| `generate_solvable_solution` | `fn(&Ident, &Option<String>) -> TokenStream` | Generates SolvableSolution/Solvable/Analyzable impls |
| `generate_shadow_support` | `fn(&ShadowConfig, &Fields, &Ident) -> Result<TokenStream, Error>` | Generates `PlanningSolution` shadow method overrides |
| `generate_collection_source_methods` | `fn(&Fields) -> TokenStream` | Generates inherent solution source methods used with ConstraintFactory::for_each |
| `generate_constraint_stream_extensions` | `fn(&Fields, &Ident) -> TokenStream` | Generates the `PlanConstraintStreams` convenience trait over the same source methods |
| `extract_option_inner_type` | `fn(&Type) -> Result<&Type, Error>` | Extracts `T` from `Option<T>` |
| `extract_collection_inner_type` | `fn(&Type) -> Option<&Type>` | Extracts `T` from `Vec<T>` |
## Internal Config Structs (`planning_solution/config.rs`, private)
### `ShadowConfig`
```rust
struct ShadowConfig {
list_owner: Option<String>,
inverse_field: Option<String>,
index_field: Option<String>,
previous_field: Option<String>,
next_field: Option<String>,
cascading_listener: Option<String>,
post_update_listener: Option<String>,
entity_aggregates: Vec<String>, // "target:sum:source" format
entity_computes: Vec<String>, // "target:method" format
}
```
## Architectural Notes
### Code Generation Targets
All generated code references types via `::solverforge::__internal::*` paths, meaning the generated code depends on the `solverforge` facade crate re-exporting core types under `__internal`. Key referenced types:
- `PlanningEntity`, `PlanningSolution`, `PlanningId`, `ProblemFact`
- `EntityDescriptor`, `SolutionDescriptor`, `ProblemFactDescriptor`, `VariableDescriptor`
- `EntityCollectionExtractor`
- `ShadowVariableKind`, `SolvableSolution`
- `ScoreDirector`, `Director`
Trait impls like `Solvable`, `Analyzable`, and `ScoreAnalysis` reference `::solverforge::*` directly.
### Proc-macro Crate Root Constraint
`lib.rs` intentionally retains the thin `#[proc_macro_attribute]` and `#[proc_macro_derive]` functions because Rust requires proc-macro exports to live at the crate root. All reusable parsing and code generation logic lives in helper modules.
### Scalar Runtime Metadata
Scalar runtime assembly is index-based and manifest-owned. `#[planning_entity]`
emits hidden per-entity scalar helpers in declaration order; that compact
`variable_index` is the generated getter/setter index. `planning_model!` reads
the declared modules and generates the `PlanningModelSupport` impl that attaches
descriptor hooks and runtime `ScalarVariableSlot` hooks by descriptor index
plus variable name, then orders runtime variables from descriptor order. The
generated runtime retains ordinary scalar candidate and legality semantics.
Rust module declaration order is not a user contract.
### Shadow Variable Update Order
When configured through `#[shadow_variable_updates]`,
`update_entity_shadows(descriptor_index, entity_idx)` collects the list owner's
`element_indices`, then updates inverse, index, previous, next, cascading,
aggregate, compute, and post-update fields in that order. `update_all_shadows()`
visits the configured list owner once.
## Test Coverage
- `tests/trybuild.rs` — compile-pass and compile-fail coverage for the public macros, including rejection of removed field arguments and unsupported shadow annotations
- `planning_entity_tests.rs` and `planning_solution_tests.rs` — token-level golden checks for generated code shape