# WaSHI (@ohtaman/washi)
> WaSHI: WebAssembly-backed Algebraic Modeler for Mathematical Optimization & Constraint Programming (LP, MIP, QP, SMT)
**WaSHI** (WebAssembly-backed, Solver-independent, High-level modeling Interface) brings the algebraic modeling style of Python's **PuLP** to the TypeScript and JavaScript ecosystem.
- Full single-file documentation reference: [llms-full.txt](llms-full.txt)
- Canonical AI agent modeling quick-reference: [getting-started/ai-agents.md](getting-started/ai-agents.md)
---
## 1. Package Imports & Deployment
### Node.js / Bundlers (Vite, Webpack, Next.js, Bun)
```ts
import {
Model,
expr,
sum,
dot,
maximize,
minimize,
and,
or,
not,
implies,
xor,
setHighsWasmUrl,
} from "@ohtaman/washi";
// Optional solver plugins (require installing peer dependencies first):
// npm install z3-solver
import "@ohtaman/washi/solvers/z3";
// npm install glpk.js
import "@ohtaman/washi/solvers/glpk";
```
### Browser ESM & Classic Script Tags
- **Browser ESM**:
- Core (HiGHS built-in): `import { Model, expr, maximize } from "./washi/washi.js";`
- GLPK plugin: `import "./washi/solvers/glpk.js";`
- Z3 plugin: `import "./washi/solvers/z3.js";`
- **Classic `` (exposes global `Washi`)
- GLPK plugin: `` (registers `"glpk"`, exposes `WashiGLPK`)
- Z3 plugin: `` (registers `"z3"`, exposes `WashiZ3`)
### WebAssembly Asset & Runtime Rules
1. **Bundled WASM by default (no implicit CDN fallback)**:
- In Node.js and modern bundlers, WebAssembly assets resolve relative to the module automatically.
- In browser deployments, deploy the full `dist/browser/` directory (JS, Worker, and `.wasm` files) together. If a local WASM URL is missing or returns an HTML SPA fallback, initialization fails immediately and reports the attempted URL—it **never** silently switches to a public CDN.
- In browser runtimes, to load `highs.wasm` from a custom location, call `setHighsWasmUrl("https://.../highs.wasm")` or pass `await m.solve({ wasmUrl: "https://.../highs.wasm" })` (ignored in Node.js, which always loads the local Node `highs` package runtime).
2. **Z3 requires a Secure Context, Cross-Origin Isolation, and CORS in browsers**:
- Z3 WebAssembly uses `SharedArrayBuffer` multithreading. Browser pages running Z3 must run in a secure context (`window.isSecureContext === true`, i.e., HTTPS or `localhost`) and be cross-origin isolated (`window.crossOriginIsolated === true`) by serving:
- `Cross-Origin-Opener-Policy: same-origin` (on the document)
- `Cross-Origin-Embedder-Policy: require-corp` (on the document)
- `Cross-Origin-Resource-Policy: cross-origin` and `Access-Control-Allow-Origin` (on cross-origin script/WASM asset responses)
- Importing `@ohtaman/washi` never auto-injects a Service Worker. When a secure context or cross-origin isolation is absent in a browser, Z3 rejects immediately with a descriptive error.
3. **Web Workers & Cancellation (`AbortSignal`)**:
- Pre-solve cancellation (`signal.aborted` before execution starts) is supported across all solvers.
- In browsers where `Worker` is available, HiGHS runs in a dedicated Web Worker by default (`useWorker: true`). Passing `{ signal }` to `m.solve({ signal })` preemptively terminates an in-flight Worker solve (or cancels queued Worker requests).
- On main-thread HiGHS (`useWorker: false` or in Node.js), cooperative mid-solve cancellation is supported when `abort()` is triggered synchronously inside an `onProgress` callback at a solver interrupt checkpoint; arbitrary asynchronous aborts from outside cannot preempt a blocked synchronous WASM thread until a callback checkpoint or completion. Single-threaded WASM solvers without interrupt hooks (GLPK, Z3) check `signal` before execution starts.
---
## 2. Canonical Modeling Workflow & Key Rules
Formulating and solving a model consists of four distinct steps:
### Step 1: Create Model & Decision Variables
```ts
const m = new Model("ExampleModel");
// Scalar variables
const x = m.continuous("x", { lb: 0, ub: 100 });
const n = m.integer("n", { lb: 0, ub: 10 });
const b = m.binary("b");
const flag = m.bool("flag"); // Propositional boolean (for Z3 / logical constraints)
// Indexed variable containers:
// - Numeric dimension -> Array
// - Key-array dimension -> Record
const vec = m.continuous(4, { lb: 0, prefix: "v" }); // Var[] (length 4)
const grid = m.binary(2, 3, { prefix: "g" }); // Var[][] (2x3)
const byItem = m.integer(["desk", "chair"] as const, { lb: 0 }); // Record<"desk" | "chair", Var>
const mixed = m.binary(["A", "B"] as const, 3); // Record<"A" | "B", Var[]>
```
- **Optional Primal Initial Values (`initialValue?: number`)**:
- Numeric variable constructors (`m.continuous`, `m.integer`, `m.binary`, and `new Var`) and the mutable `v.initialValue` property accept an optional finite primal hint (validated with tolerance `1e-6` against bounds `[lb, ub]` and integer/binary domains), kept distinct from the solved `v.value`.
- A start may be complete or partial (e.g., transferred by stable variable names when rebuilding a MIP model). Supplying `initialValue` is a hint only—it does **not** fix the variable, add a hard constraint, or guarantee solver acceptance or a runtime speedup.
- Supported by **HiGHS** (`supportsWarmStart: true`); solvers without warm-start support (**GLPK**, **Z3**) throw an error if any variable has `initialValue !== undefined`.
### Step 2: Build Expressions (`expr`, `sum`, `dot`)
- **Unified `expr` template tag**:
- Linear expression (`LinExpr`): `` expr`30 * ${x} + 50 * ${n} - 10` ``, `` expr`dot(${profit}, ${byItem}) - 20 * ${b}` ``, `` expr`sum((${price} - ${cost}) * ${byItem})` ``
- Quadratic expression (`QuadExpr`): `` expr`${x}^2 + 2 * ${x} * ${n} + ${n}^2` ``, `` expr`sum((${vec} - ${target})^2)` `` (variable-by-variable products and squares are supported for convex QP objectives).
- Linear constraint (`Constraint`): `` expr`${x} + 2 * ${n} <= 40` ``, `` expr`sum(${byItem}) <= 100` `` (supports `<=`, `>=`, `==`, and `=`).
- **Aggregations & Element-Wise Container Arithmetic**:
- `sum(containerOrItems, mapper?)` (usable inside `` expr`sum(...)` `` or standalone `sum(...)`) sums a 1D/2D `Array`, `Iterable`, or plain `Record` of `Var` / `LinExpr` / `number` (without needing `Object.values(record)`), or maps over items/counts via `sum(items, (item, i) => ...)`. Inside `` expr`sum(...)` ``, element-wise container arithmetic (`+`, `-`, `*`, `/`, `^2`) is also supported.
- `dot(a, b)` (usable inside `` expr`dot(${a}, ${b})` `` or standalone `dot(a, b)`) computes the inner product of matching 1D arrays, 2D arrays (`number[][]` / `Var[][]`, computing the Frobenius inner product $\sum_{i,j} A_{i,j} B_{i,j}$), plain records (`Record`), or mixed Array/Record containers in either operand order.
- **Fluent methods**:
- Arithmetic (on `LinExpr` and `QuadExpr`; `Var` supports `.add()`, `.sub()`, `.mul()`, `.neg()`): `.add(other)`, `.sub(other)`, `.mul(other)`, `.div(scalar)`, `.neg()`
- Comparisons (on `Var` and `LinExpr`): `.le(rhs, name?)`, `.ge(rhs, name?)`, `.eq(rhs, name?)`
- Boolean logic (on `BoolVar`, `BoolExpr`, and `Constraint` for Z3): standalone `and(...)`, `or(...)`, `not(...)`, `implies(a, b)`, `xor(a, b)` and fluent `.and(...)`, `.or(...)`, `.not()`, `.implies(...)`, `.xor(...)`
### Step 3: Register Objective & Constraints on the Model
Constructing an `expr` or `.le()` object does **not** attach it to the model until you pass it to `m.constraint(...)` or `m.objective(...)`:
```ts
// Set objective using maximize() or minimize()
m.objective(maximize(expr`30 * ${x} + 50 * ${n}`));
// Register constraints on the model (returns the registered Constraint / BoolExpr or array)
const limit = m.constraint(expr`${x} + 2 * ${n} <= 40`, "resource_limit");
// Indicator constraint (MIP Big-M reformulation: b === 1 => x <= 50)
m.indicator(b, expr`${x} <= 50`, { activeValue: 1, name: "cap_when_b" });
```
### Step 4: Execute (`solve` or `check`) & Verify (`validateSolution`)
- **Optimization (`await m.solve(options?)`)**:
- Returns `SolverStatus` (`"Optimal"`, `"Feasible"`, `"Infeasible"`, `"Unbounded"`, `"TimeLimit"`, `"IterationLimit"`, `"Error"`, `"Unknown"`, `"NotSolved"`, `"Sat"`, or `"Unsat"`). Built-in optimization solvers return the optimization statuses (`"Optimal"`, `"Feasible"`, `"Infeasible"`, `"Unbounded"`, `"TimeLimit"`, `"IterationLimit"`, `"Error"`, `"Unknown"`, `"NotSolved"`), where a time-, iteration-, or solution-limited run (or an interrupted solve) that found a valid primal incumbent returns `"Feasible"`; `"Sat"` and `"Unsat"` are included in `SolverStatus` for satisfiability/custom solver adapters (such as `MockSolver`).
- Read values via `x.value`, `expr`...`.value`, `m.getValues(x)`, `m.getValues(container)`, `m.objectiveValue`, `constr.dual`, `constr.slack`, and `x.reducedCost`.
- Call `m.validateSolution()` after solving to independently verify variable finiteness, variable bounds (`lb`/`ub`), integer/binary/boolean domains, linear constraint residuals, and logical constraints (`SolutionValidationReport`: `{ valid, complete, maxViolation, violations, ... }`). Note that `validateSolution()` checks primal feasibility of variables and constraints; it does not recompute or compare `m.objectiveValue`.
- **Satisfiability & Conflict Diagnosis (`await m.check(options?)`)**:
- Returns `CheckResult` (`{ status: "Sat" | "Unsat" | "Unknown", values: Record, conflicts?: readonly string[], unsatCore?: readonly string[], runTime?: number }`), where `conflicts` is populated when the model is proven `"Unsat"`.
- For models containing linear arithmetic constraints or logical constraints (`BoolVar`, `and`, `or`, `implies`, `xor`), `m.check()` works with any solver (including the default HiGHS and GLPK via single-solve elastic slack relaxation, automatically linearizing logical constraints into 0-1 MIP formulation) and diagnoses conflicting constraints as `conflicts` (leaving `unsatCore: undefined` for semantic rigor).
- Exact SMT unsatisfiable-core extraction (`result.unsatCore` and `result.conflicts`) is provided when using an SMT-capable solver (`{ solver: "z3" }`). Because Z3 asserts variable bounds (`lb`/`ub`) and binary `{0, 1}` domains directly while tracking registered constraints, `unsatCore` reports a subset of registered constraint names that conflicts *together with the model's fixed variable bounds/domains* (and is not guaranteed minimal).
- **No concurrent mutation during async solve/check**:
- Mutating variables, constraints, or the objective while `await m.solve()` or `await m.check()` is pending invalidates the in-flight execution and rejects the stale promise.
---
## 3. Common Pitfalls & Retired Aliases
1. DO NOT pass a Constraint to m.objective() or arithmetic methods:
- `` WRONG: m.objective(maximize(expr`${x} <= 10` as any)); -> throws Error ``
- `` WRONG: expr`${x} + 1`.add(expr`${y} <= 5` as any); -> throws Error ``
2. DO NOT pass an arithmetic expression without (<=, >=, ==) to m.constraint():
- `` WRONG: m.constraint(expr`${x} + ${y}` as any); -> throws Error ``
3. DO NOT use quadratic terms inside linear constraints:
- `` WRONG: expr`${x}^2 <= 10`; -> throws Error ("Quadratic constraints are not supported") ``
4. DO NOT use retired aliases (removed in #59 / #81):
- Use `expr` instead of retired `c` template tag
- Use `m.constraint(...)` instead of `m.constrain(...)` or `m.add(...)`
- Use `m.objective(maximize(...))` / `m.objective(minimize(...))` instead of `m.maximize(...)` / `m.minimize(...)` / `m.setObjective(...)`
- Use `await m.solve()` instead of `await m.optimize()`
- Use `.le()`, `.ge()`, `.eq()` instead of `.lessThanOrEqual()`, `.greaterThanOrEqual()`, `.equalTo()`
- Use `.value` or `m.getValues(...)` instead of `expr.getValue()`
- Use `m.variables` / `m.constraints` instead of `m.vars` / `m.constrs`
---
## 4. Canonical English Documentation Index
### Getting Started
- [AI Coding Agent Quick-Reference Guide](getting-started/ai-agents.md): This document is the **consumer modeling quick-reference** for AI coding agents and developers building optimization or satisfiability models with `@ohtaman/washi`.
- [Introduction & Installation](getting-started/index.md): **WaSHI** (WebAssembly-backed, Solver-independent, High-level modeling Interface) brings the algebraic modeling style of Python's **PuLP** to the TypeScript and JavaScript ecosystem.
- [Runtime Environments & Deployment](getting-started/runtimes.md): WaSHI runs across JavaScript and TypeScript runtimes—from Node.js, Bun, and Deno to modern bundlers, static hosting, and vanilla browser `