WASM Generation
Generate WASM modules for constraint predicates
This section covers generating WebAssembly modules for constraint predicates.
In This Section
- Expressions - Build predicate expressions with the Expression API
- Module Builder - Generate WASM modules with WasmModuleBuilder
Overview
Constraint predicates (filter conditions, joiners, weighers) are compiled to WebAssembly. The WASM module is sent to the solver service along with your domain model and constraints.
use solverforge_core::wasm::{
Expr, FieldAccessExt, WasmModuleBuilder, PredicateDefinition,
HostFunctionRegistry,
};
// Build predicate: employee assigned but missing required skill
let predicate = {
let shift = Expr::param(0);
let employee = shift.clone().get("Shift", "employee");
Expr::and(
Expr::is_not_null(employee.clone()),
Expr::not(Expr::list_contains(
employee.get("Employee", "skills"),
shift.get("Shift", "requiredSkill"),
))
)
};
// Build WASM module
let wasm = WasmModuleBuilder::new()
.with_domain_model(model)
.with_host_functions(HostFunctionRegistry::with_standard_functions())
.add_predicate(PredicateDefinition::from_expression("skillMismatch", 1, predicate))
.build()?;
Why WASM?
WebAssembly provides:
- Portability: Same predicates work across platforms
- Safety: Sandboxed execution
- Performance: Near-native speed
- No JNI: Clean HTTP interface instead of complex native bindings
Components
| Component | Purpose |
|---|
Expression | AST for predicate logic |
Expr | Fluent builder for expressions |
PredicateDefinition | Named predicate with arity |
WasmModuleBuilder | Compiles expressions to WASM |
HostFunctionRegistry | Registers host function imports |
1 - Expressions
Build predicate expressions with the Expression API
The Expression enum represents predicate logic as an abstract syntax tree. Use the Expr helper for fluent construction.
Expr Helper
The Expr struct provides static methods for building expressions:
use solverforge_core::wasm::{Expr, FieldAccessExt};
// Get first parameter
let shift = Expr::param(0);
// Access field with FieldAccessExt trait
let employee = shift.clone().get("Shift", "employee");
// Build predicate: employee != null
let predicate = Expr::is_not_null(employee);
Literals
// Integer literal
Expr::int(42)
// Boolean literal
Expr::bool(true)
Expr::bool(false)
// Null
Expr::null()
Parameter Access
Access predicate function parameters:
// param(0) - first parameter (e.g., Shift in a filter)
// param(1) - second parameter (e.g., in a join)
let first = Expr::param(0);
let second = Expr::param(1);
Field Access
Use the FieldAccessExt trait to chain field access:
use solverforge_core::wasm::FieldAccessExt;
// shift.employee
let employee = Expr::param(0).get("Shift", "employee");
// shift.employee.name (nested)
let name = Expr::param(0)
.get("Shift", "employee")
.get("Employee", "name");
// shift.start
let start = Expr::param(0).get("Shift", "start");
Comparisons
// Equal (==)
Expr::eq(left, right)
// Not equal (!=)
Expr::ne(left, right)
// Less than (<)
Expr::lt(left, right)
// Less than or equal (<=)
Expr::le(left, right)
// Greater than (>)
Expr::gt(left, right)
// Greater than or equal (>=)
Expr::ge(left, right)
Logical Operations
// AND (&&)
Expr::and(left, right)
// OR (||)
Expr::or(left, right)
// NOT (!)
Expr::not(operand)
// Null checks
Expr::is_null(operand)
Expr::is_not_null(operand)
Arithmetic
// Addition (+)
Expr::add(left, right)
// Subtraction (-)
Expr::sub(left, right)
// Multiplication (*)
Expr::mul(left, right)
// Division (/)
Expr::div(left, right)
List Operations
// Check if list contains element
Expr::list_contains(list, element)
// Example: employee.skills contains shift.requiredSkill
let skills = Expr::param(0).get("Shift", "employee").get("Employee", "skills");
let required = Expr::param(0).get("Shift", "requiredSkill");
Expr::list_contains(skills, required)
Host Function Calls
Call host-provided functions:
// Generic host call
Expr::host_call("functionName", vec![arg1, arg2])
// String equality (convenience method)
Expr::string_equals(left, right)
// Time range overlap (convenience method)
Expr::ranges_overlap(start1, end1, start2, end2)
Conditional
// if condition { then } else { else }
Expr::if_then_else(condition, then_branch, else_branch)
// Example: if (x > 0) { 1 } else { 0 }
Expr::if_then_else(
Expr::gt(Expr::param(0), Expr::int(0)),
Expr::int(1),
Expr::int(0)
)
Complete Examples
Skill Mismatch Predicate
// Returns true if employee assigned but doesn't have required skill
let shift = Expr::param(0);
let employee = shift.clone().get("Shift", "employee");
let predicate = Expr::and(
Expr::is_not_null(employee.clone()),
Expr::not(Expr::list_contains(
employee.get("Employee", "skills"),
shift.get("Shift", "requiredSkill"),
))
);
Shifts Overlap Predicate
// Returns true if two shifts overlap in time
let shift1 = Expr::param(0);
let shift2 = Expr::param(1);
let predicate = Expr::ranges_overlap(
shift1.clone().get("Shift", "start"),
shift1.get("Shift", "end"),
shift2.clone().get("Shift", "start"),
shift2.get("Shift", "end"),
);
Gap Too Small Predicate
// Returns true if gap between shifts is less than 10 hours
let shift1 = Expr::param(0);
let shift2 = Expr::param(1);
let shift1_end = shift1.get("Shift", "end");
let shift2_start = shift2.get("Shift", "start");
let gap_seconds = Expr::sub(shift2_start, shift1_end);
let gap_hours = Expr::div(gap_seconds, Expr::int(3600));
let predicate = Expr::lt(gap_hours, Expr::int(10));
Same Employee Check
// Returns true if both shifts have the same employee
let shift1 = Expr::param(0);
let shift2 = Expr::param(1);
let emp1 = shift1.get("Shift", "employee");
let emp2 = shift2.get("Shift", "employee");
let predicate = Expr::and(
Expr::and(
Expr::is_not_null(emp1.clone()),
Expr::is_not_null(emp2.clone())
),
Expr::eq(emp1, emp2)
);
API Reference
| Method | Description |
|---|
Expr::int(value) | Integer literal |
Expr::bool(value) | Boolean literal |
Expr::null() | Null value |
Expr::param(index) | Function parameter |
expr.get(class, field) | Field access |
Expr::eq(l, r) | Equal |
Expr::ne(l, r) | Not equal |
Expr::lt(l, r) | Less than |
Expr::le(l, r) | Less than or equal |
Expr::gt(l, r) | Greater than |
Expr::ge(l, r) | Greater than or equal |
Expr::and(l, r) | Logical AND |
Expr::or(l, r) | Logical OR |
Expr::not(expr) | Logical NOT |
Expr::is_null(expr) | Is null check |
Expr::is_not_null(expr) | Is not null check |
Expr::add(l, r) | Addition |
Expr::sub(l, r) | Subtraction |
Expr::mul(l, r) | Multiplication |
Expr::div(l, r) | Division |
Expr::list_contains(list, elem) | List contains |
Expr::host_call(name, args) | Host function call |
Expr::string_equals(l, r) | String comparison |
Expr::ranges_overlap(s1, e1, s2, e2) | Time overlap |
Expr::if_then_else(c, t, e) | Conditional |
2 - Module Builder
Generate WASM modules with WasmModuleBuilder
The WasmModuleBuilder compiles expressions into a WebAssembly module.
Basic Usage
use solverforge_core::wasm::{
Expr, FieldAccessExt, HostFunctionRegistry, PredicateDefinition, WasmModuleBuilder,
};
let wasm_bytes = WasmModuleBuilder::new()
.with_domain_model(model)
.with_host_functions(HostFunctionRegistry::with_standard_functions())
.add_predicate(PredicateDefinition::from_expression(
"skillMismatch",
1,
build_skill_mismatch_expression(),
))
.build()?;
Configuration
Domain Model
The domain model is required for field access layout:
let builder = WasmModuleBuilder::new()
.with_domain_model(model);
Host Functions
Register host functions that can be called from WASM:
// Standard functions: string_equals, list_contains, ranges_overlap, etc.
let builder = WasmModuleBuilder::new()
.with_host_functions(HostFunctionRegistry::with_standard_functions());
// Or start with empty registry
let builder = WasmModuleBuilder::new()
.with_host_functions(HostFunctionRegistry::new());
Memory Configuration
let builder = WasmModuleBuilder::new()
.with_initial_memory(16) // 16 pages (1 MB)
.with_max_memory(Some(256)); // Max 256 pages (16 MB)
PredicateDefinition
Define predicates to include in the module:
From Expression
// Predicate with arity (number of parameters)
PredicateDefinition::from_expression(
"skillMismatch", // Name (used in WasmFunction references)
1, // Arity (1 parameter: the Shift)
expression, // The Expression tree
)
// With explicit parameter types
use wasm_encoder::ValType;
PredicateDefinition::from_expression_with_types(
"scaleByFloat",
vec![ValType::F32], // Float parameter
expression,
)
Always True/False
// Predicate that always returns true
PredicateDefinition::always_true("alwaysMatch", 1)
Simple Comparison
use solverforge_core::wasm::{Comparison, FieldAccess};
// Equal comparison between two fields
PredicateDefinition::equal(
"sameEmployee",
FieldAccess::new(0, "Shift", "employee"),
FieldAccess::new(1, "Shift", "employee"),
)
Adding Predicates
let builder = WasmModuleBuilder::new()
.with_domain_model(model)
.with_host_functions(HostFunctionRegistry::with_standard_functions())
.add_predicate(skill_mismatch_predicate)
.add_predicate(shifts_overlap_predicate)
.add_predicate(same_employee_predicate);
Building the Module
As Bytes
let wasm_bytes: Vec<u8> = builder.build()?;
As Base64
let wasm_base64: String = builder.build_base64()?;
Generated Exports
The builder generates these exports:
| Export | Description |
|---|
memory | Linear memory for objects |
alloc | Allocate memory |
dealloc | Deallocate memory |
newList | Create new list |
getItem | Get list item |
setItem | Set list item |
size | Get list size |
append | Append to list |
insert | Insert into list |
remove | Remove from list |
get_{Class}_{field} | Field getter |
set_{Class}_{field} | Field setter (for planning variables) |
{predicate_name} | Your custom predicates |
Complete Example
use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
use solverforge_core::wasm::{
Expr, FieldAccessExt, HostFunctionRegistry, PredicateDefinition, WasmModuleBuilder,
};
// Build predicates
let skill_mismatch = {
let shift = Expr::param(0);
let employee = shift.clone().get("Shift", "employee");
Expr::and(
Expr::is_not_null(employee.clone()),
Expr::not(Expr::list_contains(
employee.get("Employee", "skills"),
shift.get("Shift", "requiredSkill"),
))
)
};
let shifts_overlap = {
let s1 = Expr::param(0);
let s2 = Expr::param(1);
let emp1 = s1.clone().get("Shift", "employee");
let emp2 = s2.clone().get("Shift", "employee");
Expr::and(
Expr::and(
Expr::is_not_null(emp1.clone()),
Expr::eq(emp1, emp2)
),
Expr::ranges_overlap(
s1.clone().get("Shift", "start"),
s1.get("Shift", "end"),
s2.clone().get("Shift", "start"),
s2.get("Shift", "end"),
)
)
};
// Build module
let wasm_bytes = WasmModuleBuilder::new()
.with_domain_model(model)
.with_host_functions(HostFunctionRegistry::with_standard_functions())
.with_initial_memory(16)
.with_max_memory(Some(256))
.add_predicate(PredicateDefinition::from_expression(
"skillMismatch", 1, skill_mismatch
))
.add_predicate(PredicateDefinition::from_expression(
"shiftsOverlap", 2, shifts_overlap
))
.build()?;
// Encode for HTTP request
let wasm_base64 = BASE64.encode(&wasm_bytes);
API Reference
| Method | Description |
|---|
WasmModuleBuilder::new() | Create builder |
.with_domain_model(model) | Set domain model |
.with_host_functions(registry) | Set host functions |
.with_initial_memory(pages) | Initial memory pages |
.with_max_memory(Some(pages)) | Max memory pages |
.add_predicate(def) | Add predicate |
.build() | Build as Vec<u8> |
.build_base64() | Build as base64 string |