A Rust-based constraint solver library that bridges language bindings to the Timefold JVM via WebAssembly and HTTP.
SolverForge enables constraint satisfaction and optimization problems to be defined in any language (Python, JavaScript, etc.) and solved using the Timefold solver engine. Instead of requiring JNI or native bindings, SolverForge:
- Generates WASM modules containing domain object accessors and constraint predicates
- Communicates via HTTP with an embedded Java service running Timefold
- Serializes solutions as JSON for language-agnostic integration
- Language-agnostic: Core library in Rust with bindings for Python, JavaScript, etc.
- No JNI complexity: Pure HTTP/JSON interface to Timefold
- WASM-based constraints: Constraint predicates compiled to WebAssembly for execution in the JVM
- Timefold compatibility: Full access to Timefold's constraint streams, moves, and solving algorithms
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Language Bindings β
β (Python, JavaScript, etc.) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β solverforge-core (Rust) β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β
β β Domain β β Constraints β β WASM β β HTTP β β
β β Model β β Streams β β Builder β β Client β β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
HTTP/JSON
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β solverforge-wasm-service (Java) β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β
β β Chicory β β Dynamic β β Timefold β β Host β β
β β WASM Runtime β β Class Gen β β Solver β β Functions β β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
solverforge/
βββ Cargo.toml # Workspace root
βββ solverforge-core/ # Core library (Rust)
β βββ src/
β βββ analysis/ # Score explanation & constraint matching
β βββ constraints/ # Constraint streams (forEach, filter, join, etc.)
β βββ domain/ # Domain model (classes, fields, annotations)
β βββ score/ # Score types (Simple, HardSoft, Bendable)
β βββ solver/ # Solver configuration & HTTP client
β βββ wasm/ # WASM module generation
βββ solverforge-python/ # Python bindings (PyO3)
β βββ src/
β βββ annotations.rs # @planning_entity, @planning_solution, etc.
β βββ collectors.rs # ConstraintCollectors (count, sum, etc.)
β βββ decorators.rs # Python decorators for domain classes
β βββ joiners.rs # Joiners (equal, lessThan, overlapping, etc.)
β βββ lambda_analyzer.rs # Python lambda β WASM function analysis
β βββ score.rs # HardSoftScore, SimpleScore, etc.
β βββ solver.rs # SolverFactory, Solver, SolverConfig
β βββ stream.rs # ConstraintFactory, Uni/Bi/TriConstraintStream
βββ solverforge-service/ # JVM lifecycle management (Rust)
β βββ src/
β βββ service.rs # EmbeddedService - starts/stops Java process
βββ solverforge-wasm-service/ # Java Quarkus service (submodule)
βββ src/main/java/ai/timefold/wasm/service/
βββ SolverResource.java # HTTP endpoints (/solve, /analyze)
βββ HostFunctionProvider.java # WASM host functions
βββ classgen/ # Dynamic bytecode generation
βββ DomainObjectClassGenerator # Domain class generation
βββ ConstraintProviderClassGenerator # Constraint stream generation
Domain objects are stored in WASM linear memory with proper alignment:
- 32-bit types (int, float, pointers): 4-byte alignment, 4-byte size
- 64-bit types (long, double, LocalDate, LocalDateTime): 8-byte alignment, 8-byte size
- Field offsets: Calculated with alignment padding to match Rust's
LayoutCalculator
Example Shift layout:
Field Offset Size Type
-----------------------------------
id 0 4 String (pointer)
employee 4 4 Employee (pointer)
location 8 4 String (pointer)
[padding] 12 4 (align for LocalDateTime)
start 16 8 LocalDateTime (i64)
end 24 8 LocalDateTime (i64)
requiredSkill 32 4 String (pointer)
-----------------------------------
Total size: 40 bytes (aligned to 8-byte boundary)
Critical: Both Rust (WASM generation) and Java (JSON parsing/serialization) must use identical alignment rules, or field reads will access garbage memory.
Define planning entities, solutions, and constraints in the host language:
# Example: Employee Scheduling (conceptual)
class Shift:
employee: Employee # @PlanningVariable
class Schedule:
employees: list[Employee] # @ProblemFactCollection, @ValueRangeProvider
shifts: list[Shift] # @PlanningEntityCollection
score: HardSoftScore # @PlanningScoreSolverForge generates a WASM module containing:
- Memory layout for domain objects
- Field accessors (getters/setters)
- Constraint predicates (filters, joiners)
- List operations (for collections)
let request = SolveRequest::new(
domain, // IndexMap of domain classes
constraints, // IndexMap of constraint streams
wasm_base64, // Base64-encoded WASM module
"alloc", // Memory allocator function
"dealloc", // Memory deallocator function
list_accessor, // List operation functions
problem_json, // JSON-serialized problem instance
)
.with_termination(TerminationConfig::new().with_seconds_spent_limit(30));POST /solve
Content-Type: application/json
{
"domain": { "Shift": {...}, "Employee": {...}, "Schedule": {...} },
"constraints": { "roomConflict": [...], "teacherConflict": [...] },
"wasm": "AGFzbQEAAAA...",
"problem": "{\"employees\": [...], \"shifts\": [...]}",
"termination": { "secondsSpentLimit": 30 }
}
- Parse WASM β Chicory runtime loads and compiles the module
- Generate Classes β Dynamic bytecode for domain objects and constraints
- Execute Solver β Timefold evaluates constraints via WASM calls
- Return Solution β JSON-serialized solution with score and stats
SolverForge provides Python bindings compatible with Timefold's Python API:
from typing import Annotated, Optional, List
from solverforge import (
planning_entity, planning_solution,
PlanningId, PlanningVariable, PlanningScore,
ValueRangeProvider, ProblemFactCollectionProperty,
PlanningEntityCollectionProperty, HardSoftScore,
)
@planning_entity
class Lesson:
id: Annotated[str, PlanningId]
subject: str
teacher: str
timeslot: Annotated[Optional['Timeslot'], PlanningVariable(value_range_provider_refs=['timeslots'])]
room: Annotated[Optional['Room'], PlanningVariable(value_range_provider_refs=['rooms'])]
@planning_solution
class Timetable:
timeslots: Annotated[List[Timeslot], ProblemFactCollectionProperty, ValueRangeProvider(id='timeslots')]
rooms: Annotated[List[Room], ProblemFactCollectionProperty, ValueRangeProvider(id='rooms')]
lessons: Annotated[List[Lesson], PlanningEntityCollectionProperty]
score: Annotated[Optional[HardSoftScore], PlanningScore]from solverforge import (
constraint_provider, ConstraintFactory,
Joiners, ConstraintCollectors, HardSoftScore,
)
@constraint_provider
def define_constraints(factory: ConstraintFactory):
return [
# Hard: No two lessons in the same room at the same time
factory.for_each_unique_pair(Lesson, Joiners.equal(lambda l: l.timeslot))
.filter(lambda a, b: a.room == b.room)
.penalize(HardSoftScore.ONE_HARD)
.as_constraint("Room conflict"),
# Hard: A teacher can only teach one lesson at a time
factory.for_each_unique_pair(Lesson, Joiners.equal(lambda l: l.timeslot))
.filter(lambda a, b: a.teacher == b.teacher)
.penalize(HardSoftScore.ONE_HARD)
.as_constraint("Teacher conflict"),
# Soft: Prefer consecutive lessons for the same teacher
factory.for_each(Lesson)
.group_by(lambda l: l.teacher, ConstraintCollectors.count())
.filter(lambda teacher, count: count > 3)
.penalize(HardSoftScore.ONE_SOFT)
.as_constraint("Teacher workload"),
]from solverforge import SolverFactory, SolverConfig, TerminationConfig
config = (SolverConfig()
.with_solution_class(Timetable)
.with_entity_classes([Lesson])
.with_termination(TerminationConfig().with_seconds_spent_limit(30)))
solver = SolverFactory.create(config, define_constraints).build()
solution = solver.solve(problem)
print(f"Score: {solution.score}")Annotations:
@planning_entity,@planning_solution,@constraint_providerPlanningId,PlanningVariable,PlanningListVariable,PlanningScoreValueRangeProvider,ProblemFactCollectionProperty,PlanningEntityCollectionPropertyPlanningPin,InverseRelationShadowVariable,DeepPlanningClone@deep_planning_clonedecorator
Constraint Streams:
UniConstraintStream,BiConstraintStream,TriConstraintStream- Operations:
filter(),join(),if_exists(),if_not_exists() - Grouping:
group_by(),group_by_collector(),group_by_two_keys() - Scoring:
penalize(),reward(),as_constraint()
Joiners:
Joiners.equal(),less_than(),less_than_or_equal()greater_than(),greater_than_or_equal(),overlapping()
Collectors:
ConstraintCollectors.count(),count_distinct(),sum(),average()min(),max(),to_list(),to_set(),load_balance()
Scores:
SimpleScore,HardSoftScore,HardMediumSoftScore
- Domain model definition with planning annotations
- Constraint streams: forEach, filter, join, groupBy, complement, flattenLast, penalize, reward
- WASM module generation for constraint predicates with proper memory alignment
- End-to-end solving via HTTP with embedded Java service
- Score types: Simple, HardSoft, HardMediumSoft, Bendable, HardSoftBigDecimal
- Score analysis with constraint breakdown
- Primitive list support: flattenLast works with LocalDate[] and other primitive lists
- Advanced collectors: count, countDistinct, loadBalance
- Python bindings (PyO3): Full Timefold-compatible API
- Decorators:
@planning_entity,@planning_solution,@constraint_provider - Annotations:
PlanningVariable,PlanningScore,ValueRangeProvider, etc. - Constraint streams:
UniConstraintStream,BiConstraintStream,TriConstraintStream - Joiners:
equal,lessThan,overlapping, etc. - Collectors:
count,sum,average,toList,loadBalance, etc. - Lambda analysis: Python lambdas β WASM functions
- Decorators:
| Metric | Current | Target | Native Timefold |
|---|---|---|---|
| Moves/second | ~500 | 50,000+ | ~100,000 |
Known Bottlenecks (optimization plan in progress):
- No WASM module caching - recompiled every request
- No export function caching - string lookup per call
- Full constraint re-evaluation - no incremental scoring
- No join indexing - O(n*m) scans instead of O(1) lookups
- Memory alignment fix (2025-12): Fixed field offset alignment mismatch between Java and Rust. Java now properly aligns 64-bit fields (long, double, LocalDate, LocalDateTime) to 8-byte boundaries, matching Rust's LayoutCalculator behavior. This resolved "out of bounds memory access" errors when using temporal types in domain models.
All tests passing:
# Build
cargo build --workspace
# Run all tests (requires Java 24)
cargo test --workspace
# Run Python bindings tests
make test-python
# Run specific integration test
cargo test -p solverforge-service test_employee_scheduling_solve
# Run with specific Java version
JAVA_HOME=/usr/lib64/jvm/java-24-openjdk-24 \
cargo test -p solverforge-service --test solve_integrationTest Counts:
- solverforge-core: 478 tests
- solverforge-python: 129 tests
Integration Tests:
- β Employee scheduling with 5 constraints (requiredSkill, noOverlappingShifts, oneShiftPerDay, atLeast10HoursBetweenTwoShifts, balanceEmployeeShiftAssignments)
- β Primitive list operations (LocalDate[] with flattenLast)
- β Advanced collectors (loadBalance for fair distribution)
- β Weighted penalties and custom weighers
- β Python domain model extraction from decorated classes
- β Python constraint stream building with lambda analysis
- β TriConstraintStream for 3-entity joins
- β GroupBy operations with collectors
- Rust: 1.75+ (edition 2021)
- Java: 24+ (for solverforge-wasm-service)
- Maven: 3.9+ (for building Java service)
- Python: 3.10+ (tested on 3.10, 3.11, 3.12, 3.13)
- maturin: 1.8+ (for building Python wheel)
Apache-2.0