packages feed

shikumi-compile-0.2.0.0: src/Shikumi/Compile/Serialize.hs

-- | Serialization of a 'CompiledProgram'.
--
-- __Parameter-state plus shape fingerprint, not whole-structure.__ A @Program@'s
-- executable structure cannot be serialized in general (its @FMap@ nodes hold
-- opaque functions and its GADT type indices are not reflected at runtime). What
-- /can/ be saved — and is exactly what the optimizer (EP-10) and CLI (EP-12)
-- need — is each node's tunable 'Shikumi.Program.Params', plus a closure-free
-- structural fingerprint used to reject the wrong /template/. This mirrors DSPy's
-- @dump_state@/@load_state@: dump the parameters, load them back onto a program you
-- still hold as code.
--
-- This reuses EP-4's already-provided parameter interface verbatim:
-- 'Shikumi.Program.programShape', 'Shikumi.Program.programParams' (the ordered
-- @[Params]@ vector, in @foldParams@ order), and
-- 'Shikumi.Program.setProgramParams' (re-apply a vector onto a matching program,
-- with a length-mismatch guard). So this module is a thin JSON wrapper — the shape
-- and node-order contracts are EP-4's, not re-invented here.
module Shikumi.Compile.Serialize
  ( encodeCompiled,
    decodeCompiledOnto,
  )
where

import Data.Aeson (FromJSON, ToJSON, eitherDecode, encode)
import Data.ByteString.Lazy (ByteString)
import GHC.Generics (Generic)
import Shikumi.Compile.Types (CompiledProgram (..))
import Shikumi.Program
  ( Params,
    Program,
    ProgramShape,
    ProgramShapeError (..),
    programParams,
    programShape,
    setProgramParams,
  )

-- | The persisted form of a compiled program: a closure-free structural
-- fingerprint plus the ordered parameter vector.
data CompiledState = CompiledState
  { shape :: !ProgramShape,
    params :: ![Params]
  }
  deriving stock (Generic)

instance ToJSON CompiledState

instance FromJSON CompiledState

-- | Emit a compiled program's shape fingerprint and ordered parameter vector as
-- JSON.
encodeCompiled :: CompiledProgram i o -> ByteString
encodeCompiled (CompiledProgram p) = encode (CompiledState (programShape p) (programParams p))

-- | Re-apply saved state onto a structural /template/ (the program, held as code),
-- in node order. Fails with a descriptive message if the JSON is malformed, if the
-- shape fingerprint does not match the template, or if the parameter count is
-- inconsistent with the template. The template must have the same compiled shape
-- that produced the saved state: for structural compilers such as
-- 'Shikumi.Compile.ChainOfThought.chainOfThoughtCompiler', re-apply the same
-- compiler to the base program before loading. The signature mirrors DSPy's
-- @load_state(program)@.
decodeCompiledOnto ::
  Program i o ->
  ByteString ->
  Either String (CompiledProgram i o)
decodeCompiledOnto template bs = do
  st <-
    either
      (Left . decodeError)
      Right
      (eitherDecode bs :: Either String CompiledState)
  let templateShape = programShape template
  if shape st /= templateShape
    then
      Left
        ( "shikumi-compile: shape mismatch — the saved state was produced by a structurally different program. saved shape: "
            <> show (shape st)
            <> "; template shape: "
            <> show templateShape
        )
    else applyParams (params st)
  where
    decodeError e =
      "shikumi-compile: expected a compiled-state envelope with shape and params; "
        <> "legacy bare parameter arrays are not supported and must be re-encoded. Aeson error: "
        <> e
    applyParams ps =
      case setProgramParams ps template of
        Left (ParamCountMismatch (expected, got)) ->
          Left
            ( "shikumi-compile: parameter count mismatch — the template has "
                <> show expected
                <> " node(s) but the saved state has "
                <> show got
            )
        Right prog -> Right (CompiledProgram prog)