# ph-haptics Compiler Design (`ph-haptics-gen`)
## Purpose
`ph-haptics-gen` compiles textual haptics definitions (`.phh`) plus optional motor profile TOML into static Rust catalogs that the `ph-haptics` runtime evaluates into timed, abstract motor commands on embedded targets.
The compiler is intentionally split into focused modules so parsing, validation, semantic resolution, and code generation can be tested independently.
## Scope and Boundaries
- In scope:
- Parse and validate `.phh` haptics definitions (including `include` expansion).
- Parse and merge motor profile presets from TOML.
- Resolve curve references and emit compiled Rust assets.
- Integrate generated assets with `ph-curves` curve symbols.
- Out of scope:
- Runtime command evaluation and scheduling (handled by `ph-haptics::Runner`).
- Curve LUT generation from equations (handled by `ph-curves-gen`).
- Kick pulse injection (handled at runtime when recovering from rest / level 0).
- Peripheral control, motor-driver integration, physical actuation, and HIL evidence (owned by consuming firmware).
## Module Layout
- `src/bin/ph-haptics-gen.rs`:
- Thin orchestrator (`CLI -> include expand -> parse -> validate -> generate -> write`).
- `src/bin/ph_haptics_gen/cli.rs`:
- CLI argument parsing and help text.
- `src/bin/ph_haptics_gen/model.rs`:
- Internal AST/domain types (`Haptic`, `Inst`, `Profile`, etc.).
- `src/bin/ph_haptics_gen/parser.rs`:
- `.phh` parser and optional curve-symbol extraction from generated curves file.
- `src/bin/ph_haptics_gen/profiles.rs`:
- TOML profile parsing and field-level merge with `DEFAULT_ERM_PROFILE` / `DEFAULT_LRA_PROFILE`.
- `src/bin/ph_haptics_gen/semantic.rs`:
- Profile binding per haptic and curve symbol resolution (`name` vs `@gamma`).
- `src/bin/ph_haptics_gen/validate.rs`:
- Semantic checks before codegen.
- `src/bin/ph_haptics_gen/codegen.rs`:
- Rust source emission (`MotorProfile`, instructions, programs, catalog).
- `src/bin/ph_haptics_gen/names.rs`, `ident.rs`:
- Name validation and Rust constant identifier normalization.
## Inputs
### 1) Haptics source (`.phh`)
Top-level directives:
- `haptic <name> motor=<erm|lra> [profile=<name>] [loop=<once|forever|N>]`
- `include "relative/path.phh"` — expanded before parsing (see pipeline)
Instruction format inside a `haptic` block:
- `ramp <duration_ms> <from%> <to%> <curve|@gamma> [step=<u16>] [rounding=<nearest|floor|ceil>] [min_dt=<u32>] [lra_hz=<u16>] [lra_hz_to=<u16>]`
- `hold <duration_ms> <level%> [lra_hz=<u16>]`
- `pause <duration_ms>`
- `use <name>` — inline a previously defined haptic (same motor kind; cross-motor rejected)
- `repeat N` / `endrepeat` — duplicate enclosed instructions (no nesting; N ≤ 1024; total instructions ≤ 10_000)
- `end`
Parser traits:
- line-oriented
- supports comments via `#`
- fails with line-numbered errors
- levels are percentages `0..100` (mapped to `0..=u16::MAX` at codegen)
### 2) Built-in profile pack + optional TOML overlay (`--profiles-toml`)
Built-in profile pack:
- `assets/phh-library/motor_profiles.toml` (always loaded)
Optional overlay:
- user-supplied TOML via `--profiles-toml`
- merged over built-ins by profile name (field-level: only `Some` overlay fields override)
Parsed TOML shape (`[profiles.<name>]`):
- `motor = "erm" | "lra"` selects the motor family.
- `kick_ms`, `kick_level`, `min_level`, `max_level`, `gamma`, `ramp_step_ms`, `min_dt_ms`, `duty_step`
- **Level/duty fields are percentages (0..100)** in TOML.
- Codegen converts them to runtime `MotorProfile` fraction fields: `kick_frac`, `min_run_frac`, `max_frac`, `duty_step` (`0..=255`).
Missing fields inherit from:
- the selected motor family's defaults when `motor` is present;
- the existing built-in profile when overlaying a profile without changing its
motor family; or
- the legacy name fallback for a new profile without `motor`:
`DEFAULT_LRA_PROFILE` when the name starts with `lra_`, and
`DEFAULT_ERM_PROFILE` otherwise.
Set `motor` explicitly for new profiles. An overlay that changes a built-in
profile's motor family is retuned from the new family's defaults before its
fields are applied.
### 3) Optional curve symbol source (`--curves-file`)
If provided, parser extracts `pub const <SYMBOL>...` names and validation enforces that every referenced ramp curve exists. Omit this flag when you do not have a `ph-curves-gen` output file on hand.
## Compilation Pipeline
1. Parse CLI flags (`cli::parse_cli`); validate `--curves-module` / `--haptics-crate` as Rust paths.
2. Canonicalize the top-level `.phh` input; jail root = its parent directory.
3. **Expand `include` directives** (`resolve_includes`):
- relative paths only; absolute paths rejected
- includes must stay under the jail root
- `#` trailing comments stripped on include lines
- each file is expanded at most once, so diamond includes inline the shared
file a single time instead of producing duplicate haptics
- a separate recursion stack detects genuine cycles and errors
- a source map records the origin of every expanded line, so diagnostics
report `file:line` from the original `.phh` rather than an offset into the
concatenated buffer
4. Load built-in profile presets, then optional overlay (field-level merge).
5. Parse expanded `.phh` into typed AST (`Vec<Haptic>`), including `use` / `repeat` expansion.
6. If `--curves-file` is set, index curve symbols.
7. Validate semantics:
- every haptic has at least one instruction
- profile references exist
- `@gamma` is only used when profile gamma is present
- optional strict curve symbol presence check
- instruction / repeat caps
8. Generate Rust output (percent → frac for profiles; no synthetic kick `Hold`s).
9. Write output file.
## Semantic Transformations During Codegen
### Profile binding
For each haptic:
- explicit `profile=<name>` -> bound profile
- ERM with no profile -> implicit `DEFAULT_ERM_PROFILE`
- LRA with no profile -> implicit `DEFAULT_LRA_PROFILE`
### Level clamping
Non-zero levels are clamped to profile range:
- TOML `[min_level, max_level]` → runtime `[min_run_frac, max_frac]`
This applies to ramp endpoints and hold levels when a profile is active.
### Ramp default filling
If not explicitly provided:
- `step` defaults from profile `duty_step` (percentage → `u16` domain)
- `min_dt` defaults from `min_dt_ms` or `ramp_step_ms` fallback
- `rounding` defaults to `nearest`
### Kick (runtime, not codegen)
The generator does **not** inject synthetic `Instruction::Hold` for kick.
`Runner` applies kick pulses during command evaluation when the scheduled level recovers from rest or
returns to level 0 (`kick_ms` + `kick_frac` from the bound profile).
### Gamma curve reference handling
`@gamma` in a ramp curve slot resolves to profile `gamma` curve symbol at compile time.
### Frequency sweeps
`lra_hz_to` on a ramp records `lra_frequency_hz_to` on the compiled instruction.
Runtime lerps frequency across the ramp and schedules tickless wakeups when the
quantized `u16` Hz value changes.
### Tickless scheduling (wall-clock, wrap-safe)
With `ph-curves` ≥ 0.2.1, ramp evaluation feeds wall-clock time into
`TicklessSchedule` (`t0_ms = segment_start_ms`, `next_deadline(now_ms)`). LRA Hz
lerp stays segment-relative; amplitude and Hz wakeups merge via offset `.min()`
then `wrapping_add` — absolute `.min()` on wrapped timestamps is wrong. Kick
expiry likewise uses remaining-time half-range checks so a `u32` clock wrap
cannot leave a kick stuck on.
## Generated Output Structure
Generated Rust contains:
1. `use` imports for `ph-haptics` and `ph-curves` types.
2. One `MotorProfile` static per TOML profile referenced by a haptic.
3. For each haptic:
- instruction array static
- `Program` const
- `CompiledHapticDef` const
4. `COMPILED_HAPTICS` aggregate array.
5. `COMPILED_CATALOG` convenience constant.
## Integration with `ph-curves`
Dependency floor is **`ph-curves` 0.2.1** with default features off. The runtime
re-exports the existing curve / tickless passthrough surface only; 0.2
transfer / filter / calibration APIs are intentionally not re-exported.
### Compile-time integration
- Curves are referenced as Rust symbols (typically generated by `ph-curves-gen`).
- Curve tokens in `.phh` are normalized into Rust constant identifiers.
- Emitted instruction arrays are typed as:
- `Instruction<MonotonicCurveLut256>`
- Emitted ramp rounding uses:
- `ph_curves::Rounding`
- Normalized haptic / profile idents are collision-checked before emit
(parallel to ph-curves 0.2 rejecting colliding normalized curve/transfer
idents).
### Optional strict symbol verification
With `--curves-file`, the compiler verifies that every resolved ramp curve symbol appears in generated curves source (`pub const ...`), preventing stale/misspelled curve names from compiling.
### Runtime integration path
Generated `CompiledHapticDef` carries optional `MotorProfile` reference.
During command evaluation, `Runner` consumes this profile and:
- applies kick when recovering from rest / level 0
- applies profile gamma LUT mapping (`profile.gamma_curve`) to output levels
- before motor scaling/drive command emission
This means curve integration occurs in two places:
- ramp-shape curves: compile-time into instructions
- perceptual output gamma: runtime via profile carried by compiled definitions
## Error Model
The compiler is fail-fast and returns `Result<_, String>` errors with context.
Typical failures:
- malformed `.phh` syntax
- include jail / cycle / absolute-path errors
- missing required tokens
- invalid numbers/options
- unknown profile references
- invalid `@gamma` usage
- cross-motor `use`
- repeat / instruction count caps
- unknown curves when strict symbol validation is enabled
- file I/O failures
## Testability Notes
Module-level unit tests cover:
- CLI behavior and path validation
- parser for multiple haptics, `use`, `repeat`, includes
- profile fallback to default ERM / LRA values and field-level overlays
- semantic validation failures
- key codegen expectations
Design intent is that each phase is unit-testable in isolation, with minimal coupling between parsing, validation, and rendering.
## Practical Build Flow with `ph-curves`
1. Generate curves with `ph-curves-gen` (e.g. `src/curves.rs` in your firmware crate).
2. Author `.phh` haptics and optional profiles TOML (or use `assets/phh-library/`).
3. Run `ph-haptics-gen` with:
- `--curves-module` for import path
- `--curves-file` for strict symbol validation when that file exists (recommended in firmware packages)
4. Include generated compiled file in firmware/app.
5. Use `COMPILED_CATALOG` + `Runner` in runtime.