hanalyze-0.2.0.0: src/Hanalyze/Model/HBM/Ast.hs
{-# LANGUAGE OverloadedStrings #-}
-- |
-- Module : Hanalyze.Model.HBM.Ast
-- Description : HBM dialog DSL の AST 型と JSON decoder
-- Copyright : (c) 2026 Aelysce Project (Toshiaki Honda)
-- License : BSD-3-Clause
--
-- HBM dialog DSL の AST 型と JSON decoder。
--
-- Phase 27.5 (2026-05-31): canvas-backend @フロントエンド app.Analysis.HBM@ から
-- 移設。 frontend が backend 統一 parser (@/api/v1/dsl/parse@) から得た
-- @program_ast@ (JSON) を、 streaming sidecar が直接 decode して実モデルを
-- 構築できるよう、 AST 型 + 'parseAst' をライブラリ層 (hanalyze) に置く。
--
-- 本 module は **canvas wire 型にも text parser (DSL frontend) にも依存しない**
-- (= aeson のみ)。 text → AST 変換 ('parseHbmTextToExpr' 等) は HT (DSL frontend)
-- に依存するため canvas-backend 側に残す。
module Hanalyze.Model.HBM.Ast
( -- * AST
Expr (..)
, Lit (..)
, Bind (..)
, DoStmt (..)
-- * JSON decode (= frontend program_ast → Expr)
, parseAst
, parseLit
, parseBind
, parseDoStmt
-- * JSON encode (= 'parseAst' の正確な逆。 backend が sidecar に
-- program_ast / top_binds を送る際に使う、 Phase 27.5 step 3)
, exprToJSON
, litToJSON
, bindToJSON
, doStmtToJSON
-- * helpers
, Err
, collectApp
, getField
, getStr
, getNum
, getBool
, getArray
) where
import Data.Text (Text)
import qualified Data.Aeson as A
import Data.Aeson.Types (Pair)
import qualified Data.Aeson.Key as Key
import qualified Data.Aeson.KeyMap as KM
import qualified Data.Vector as V
-- ---------------------------------------------------------------------------
-- AST (= frontend App.Hbm.Ast / DSL frontend hanalyze.HBM.Text.HbmExpr と同形、
-- 11 ctor: ELit / ECol / EVar / EApp / ELam / EIf / ELet / ENeg / EOp /
-- EList / EDo)
-- ---------------------------------------------------------------------------
data Expr
= ELit Lit
| ECol Text
| EVar Text
| EApp Expr Expr
| ELam Text Expr
| EIf Expr Expr Expr
| ELet [Bind] Expr
| ENeg Expr
| EOp Text Expr Expr
| EList [Expr]
| EDo [DoStmt] Expr
deriving (Show)
data Lit = LNumber Double | LText Text | LBool Bool deriving (Show)
data Bind = Bind { bindName :: Text, bindValue :: Expr } deriving (Show)
data DoStmt
= DoBind Text Expr
| DoLet [Bind]
| DoExpr Expr
deriving (Show)
-- | 評価系で多用する Either alias。
type Err a = Either Text a
-- ---------------------------------------------------------------------------
-- JSON parser (= frontend が送る program_ast を Expr に decode)
-- ---------------------------------------------------------------------------
parseAst :: A.Value -> Either Text Expr
parseAst v = case v of
A.Object o -> do
tag <- getStr o "tag"
case tag of
"ELit" -> ELit <$> (parseLit =<< getField o "lit")
"ECol" -> ECol <$> getStr o "name"
"EVar" -> EVar <$> getStr o "name"
"EApp" -> EApp <$> (parseAst =<< getField o "f") <*> (parseAst =<< getField o "x")
"ELam" -> ELam <$> getStr o "arg" <*> (parseAst =<< getField o "body")
"EIf" -> EIf <$> (parseAst =<< getField o "c")
<*> (parseAst =<< getField o "a")
<*> (parseAst =<< getField o "b")
"ELet" -> do
bs <- getArray o "binds" >>= mapM parseBind
body <- parseAst =<< getField o "body"
Right (ELet bs body)
"ENeg" -> ENeg <$> (parseAst =<< getField o "e")
"EOp" -> EOp <$> getStr o "op"
<*> (parseAst =<< getField o "a")
<*> (parseAst =<< getField o "b")
"EList" -> EList <$> (getArray o "items" >>= mapM parseAst)
"EDo" -> do
stmts <- getArray o "stmts" >>= mapM parseDoStmt
ret <- parseAst =<< getField o "ret"
Right (EDo stmts ret)
_ -> Left ("Unknown AST tag: " <> tag)
_ -> Left "AST root must be a JSON object"
parseLit :: A.Value -> Either Text Lit
parseLit v = case v of
A.Object o -> do
tag <- getStr o "tag"
case tag of
"LNumber" -> do
n <- getNum o "value"
Right (LNumber n)
"LText" -> LText <$> getStr o "value"
"LBool" -> LBool <$> getBool o "value"
_ -> Left ("Unknown literal tag: " <> tag)
_ -> Left "Literal must be an object"
parseBind :: A.Value -> Either Text Bind
parseBind v = case v of
A.Object o -> do
n <- getStr o "name"
e <- parseAst =<< getField o "value"
Right (Bind n e)
_ -> Left "Bind must be an object"
parseDoStmt :: A.Value -> Either Text DoStmt
parseDoStmt v = case v of
A.Object o -> do
tag <- getStr o "tag"
case tag of
"DoBind" -> do
name <- getStr o "name"
rawValue <- parseAst =<< getField o "value"
-- Phase 9.1d-4 fix: frontend が `x <- sample "obsName" dist` を
-- DoBind の value に raw expression として渡してくる。 validateStmts
-- 以降は value が「純粋な distribution」 であることを期待するので、
-- ここで sample wrapper を剥がす。 sample 形でなければそのまま通す
-- (互換: 直接 dist を入れた古い経路があった場合のため)。
let distOnly = case collectApp rawValue of
Right ("sample", [ELit (LText _samplerName), d]) -> d
_ -> rawValue
pure (DoBind name distOnly)
"DoLet" -> DoLet <$> (getArray o "binds" >>= mapM parseBind)
"DoExpr" -> DoExpr <$> (parseAst =<< getField o "value")
_ -> Left ("Unknown DoStmt tag: " <> tag)
_ -> Left "DoStmt must be an object"
-- ---------------------------------------------------------------------------
-- JSON encoder (= parseAst の正確な逆。 round-trip: parseAst . exprToJSON ≡ Right)
--
-- Phase 27.5 step 3 (2026-06-01): topology B で backend が stream sidecar に
-- start.params を組む際、 resolveHbmModel が返す Expr / TopBind を worker
-- (= parseAst で decode) が読める JSON 文字列に直す必要がある。 decoder と
-- 同じ module に逆変換を置き、 tag / field 名のズレを構造的に防ぐ。
--
-- 注: DoBind の value は sample wrapper を剥がした dist-only を前提とする
-- (parseDoStmt は sample wrapper を剥がすが、 既に剥がれた式には作用しない =
-- idempotent。 resolveHbmModel 経由の Expr は剥がし済)。
-- ---------------------------------------------------------------------------
exprToJSON :: Expr -> A.Value
exprToJSON e = case e of
ELit l -> obj "ELit" ["lit" A..= litToJSON l]
ECol n -> obj "ECol" ["name" A..= n]
EVar n -> obj "EVar" ["name" A..= n]
EApp f x -> obj "EApp" ["f" A..= exprToJSON f, "x" A..= exprToJSON x]
ELam a b -> obj "ELam" ["arg" A..= a, "body" A..= exprToJSON b]
EIf c a b -> obj "EIf" ["c" A..= exprToJSON c, "a" A..= exprToJSON a, "b" A..= exprToJSON b]
ELet bs body -> obj "ELet" ["binds" A..= map bindToJSON bs, "body" A..= exprToJSON body]
ENeg x -> obj "ENeg" ["e" A..= exprToJSON x]
EOp op a b -> obj "EOp" ["op" A..= op, "a" A..= exprToJSON a, "b" A..= exprToJSON b]
EList xs -> obj "EList" ["items" A..= map exprToJSON xs]
EDo stmts r -> obj "EDo" ["stmts" A..= map doStmtToJSON stmts, "ret" A..= exprToJSON r]
where
obj :: Text -> [Pair] -> A.Value
obj tag fields = A.object (("tag" A..= tag) : fields)
litToJSON :: Lit -> A.Value
litToJSON l = case l of
LNumber n -> A.object ["tag" A..= ("LNumber" :: Text), "value" A..= n]
LText t -> A.object ["tag" A..= ("LText" :: Text), "value" A..= t]
LBool b -> A.object ["tag" A..= ("LBool" :: Text), "value" A..= b]
bindToJSON :: Bind -> A.Value
bindToJSON (Bind n v) = A.object ["name" A..= n, "value" A..= exprToJSON v]
doStmtToJSON :: DoStmt -> A.Value
doStmtToJSON s = case s of
DoBind n v -> A.object ["tag" A..= ("DoBind" :: Text), "name" A..= n, "value" A..= exprToJSON v]
DoLet bs -> A.object ["tag" A..= ("DoLet" :: Text), "binds" A..= map bindToJSON bs]
DoExpr v -> A.object ["tag" A..= ("DoExpr" :: Text), "value" A..= exprToJSON v]
-- | @EApp (EApp (EVar f) a) b@ → @(f, [a, b])@。 distribution / 関数適用の
-- head + 引数列を取り出す。 head が変数でなければ Left。
collectApp :: Expr -> Err (Text, [Expr])
collectApp e0 = go e0 []
where
go (EVar n) acc = Right (n, acc)
go (EApp f x) acc = go f (x : acc)
go _ _ = Left "Distribution must be a function applied to scalar args"
-- ---------------------------------------------------------------------------
-- helpers
-- ---------------------------------------------------------------------------
getField :: A.Object -> Text -> Either Text A.Value
getField o k = case KM.lookup (Key.fromText k) o of
Just v -> Right v
Nothing -> Left ("Missing field: " <> k)
getStr :: A.Object -> Text -> Either Text Text
getStr o k = case KM.lookup (Key.fromText k) o of
Just (A.String s) -> Right s
_ -> Left ("Field not string: " <> k)
getNum :: A.Object -> Text -> Either Text Double
getNum o k = case KM.lookup (Key.fromText k) o of
Just (A.Number n) -> Right (realToFrac n)
_ -> Left ("Field not number: " <> k)
getBool :: A.Object -> Text -> Either Text Bool
getBool o k = case KM.lookup (Key.fromText k) o of
Just (A.Bool b) -> Right b
_ -> Left ("Field not bool: " <> k)
getArray :: A.Object -> Text -> Either Text [A.Value]
getArray o k = case KM.lookup (Key.fromText k) o of
Just (A.Array xs) -> Right (V.toList xs)
_ -> Left ("Field not array: " <> k)