solverforge-macros 0.19.4

Derive macros for SolverForge constraint solver
Documentation
# 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