This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

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

ComponentPurpose
ExpressionAST for predicate logic
ExprFluent builder for expressions
PredicateDefinitionNamed predicate with arity
WasmModuleBuilderCompiles expressions to WASM
HostFunctionRegistryRegisters 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

MethodDescription
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:

ExportDescription
memoryLinear memory for objects
allocAllocate memory
deallocDeallocate memory
newListCreate new list
getItemGet list item
setItemSet list item
sizeGet list size
appendAppend to list
insertInsert into list
removeRemove 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

MethodDescription
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