bootstrap: Rust crate skeleton — IR v2 deserialization + contract tests against IR fixtures #1

Open
opened 2026-06-11 15:38:53 +02:00 by yorunikakeru · 1 comment
Owner

Repo is empty. First milestone for the Planner:

  1. Cargo crate skeleton (lib + actor binary later), CI mirroring DSL/IR (.forgejo workflow, fmt/clippy/test).
  2. serde types mirroring IR v2: IRDocument, ProfileSection, ServiceSection, Condition (process_running / cpu_load / battery_below / battery_above / and / not), Action (service_enable / service_disable with priority, power_profile), Policy (allow_ports, fallback), ObserveStrategy (poll).
  3. Version check on load: reject unsupported version with a typed error (see FrogOS/IR#2 — Haskell side doesn't enforce it either yet).
  4. Contract tests consuming golden fixtures from the IR repo (see FrogOS/IR#4) — deserialize, re-serialize, compare. This is the tripwire for Haskell/Rust drift.

Blocked-by decisions: who resolves policies (FrogOS/spec#2) shapes whether Policy lands in Planner types or gets forwarded.

Repo is empty. First milestone for the Planner: 1. Cargo crate skeleton (lib + actor binary later), CI mirroring DSL/IR (.forgejo workflow, fmt/clippy/test). 2. serde types mirroring IR v2: IRDocument, ProfileSection, ServiceSection, Condition (process_running / cpu_load / battery_below / battery_above / and / not), Action (service_enable / service_disable with priority, power_profile), Policy (allow_ports, fallback), ObserveStrategy (poll). 3. Version check on load: reject unsupported version with a typed error (see FrogOS/IR#2 — Haskell side doesn't enforce it either yet). 4. Contract tests consuming golden fixtures from the IR repo (see FrogOS/IR#4) — deserialize, re-serialize, compare. This is the tripwire for Haskell/Rust drift. Blocked-by decisions: who resolves policies (FrogOS/spec#2) shapes whether Policy lands in Planner types or gets forwarded.
Author
Owner

Architecture correction: Planner core is written in Haskell, compiled by GHC to a native library, and
linked into the Rust supervisor via FFI.
The Rust side is only a thin tokio actor wrapper so Planner
participates in the actor model (channels to Executor etc.). This reshapes the bootstrap:

  1. No Rust serde types for IR. The Haskell core consumes intent.json with the existing aeson FromJSON
    instances from frogos-ir — IR stays a Haskell-internal contract. Rust passes IR as opaque bytes.
  2. Haskell package layout: pure core `plan :: IRDocument -> SystemState -> Either PlanError Plan`
    (depends on frogos-ir directly — no type duplication with DSL), plus an FFI surface module with `foreign
    export ccall` wrappers (bytes in, bytes out).
  3. The real cross-language contracts are SystemState (Rust → Haskell) and Plan (Haskell → Rust), both
    crossing the FFI boundary — define them as versioned JSON like IR, with golden fixtures in this repo.
    Spec's "Plan — Rust, только в памяти" needs updating: Plan is born in Haskell and crosses into Rust.
  4. GHC RTS embedding work: Rust calls hs_init/hs_exit once per process; calls into Haskell from tokio
    must go through spawn_blocking (FFI call may block on GC); exceptions must never escape foreign export
    (catch all, return error payload); buffer ownership protocol (Haskell allocates → Rust copies → Haskell
    frees, or malloc-based handoff).
  5. CI: build the GHC static lib + a small Rust harness that links it and runs roundtrip contract tests
    (SystemState in, Plan out) in one process.

Latency note worth tracking: GC pauses inside the reconciliation hot path — likely fine for this workload,
but measure once the loop exists.

**Architecture correction: Planner core is written in Haskell, compiled by GHC to a native library, and linked into the Rust supervisor via FFI.** The Rust side is only a thin tokio actor wrapper so Planner participates in the actor model (channels to Executor etc.). This reshapes the bootstrap: 1. **No Rust serde types for IR.** The Haskell core consumes intent.json with the existing aeson FromJSON instances from frogos-ir — IR stays a Haskell-internal contract. Rust passes IR as opaque bytes. 2. **Haskell package layout**: pure core \`plan :: IRDocument -> SystemState -> Either PlanError Plan\` (depends on frogos-ir directly — no type duplication with DSL), plus an FFI surface module with \`foreign export ccall\` wrappers (bytes in, bytes out). 3. **The real cross-language contracts are SystemState (Rust → Haskell) and Plan (Haskell → Rust)**, both crossing the FFI boundary — define them as versioned JSON like IR, with golden fixtures in this repo. Spec's "Plan — Rust, только в памяти" needs updating: Plan is born in Haskell and crosses into Rust. 4. **GHC RTS embedding work**: Rust calls hs_init/hs_exit once per process; calls into Haskell from tokio must go through spawn_blocking (FFI call may block on GC); exceptions must never escape foreign export (catch all, return error payload); buffer ownership protocol (Haskell allocates → Rust copies → Haskell frees, or malloc-based handoff). 5. **CI**: build the GHC static lib + a small Rust harness that links it and runs roundtrip contract tests (SystemState in, Plan out) in one process. Latency note worth tracking: GC pauses inside the reconciliation hot path — likely fine for this workload, but measure once the loop exists.
Sign in to join this conversation.
No labels
core
ffi
haskell
rust
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
FrogOS/Planner#1
No description provided.