packages feed

cabal-buck2-0.1.0.0: src/Distribution/Client/Buck2/Starlark.hs

-- | A minimal Starlark (BUCK file) pretty-printer.
--
-- This deliberately doesn't attempt to represent the whole Starlark
-- language: it covers exactly the subset (string/int/bool scalars, lists,
-- dicts, @load@ statements and top-level rule calls) that generated
-- @BUCK@\/@.bzl@ files need, so every generator in "Distribution.Client.Buck2"
-- builds output through the same renderer instead of hand-formatting
-- strings.
module Distribution.Client.Buck2.Starlark
  ( Value (..)
  , str
  , strList
  , Call (..)
  , call
  , renderCall
  , renderLoad
  , renderBinding
  , renderFile
  ) where

import Distribution.Client.Compat.Prelude
import Prelude ()

-- | A Starlark expression, restricted to what BUCK files actually need:
-- scalars and (recursively) lists and dicts of them.
data Value
  = VStr String
  | VInt Int
  | VBool Bool
  | VList [Value]
  | VDict [(String, Value)]

str :: String -> Value
str = VStr

strList :: [String] -> Value
strList = VList . map VStr

-- | A single top-level rule invocation, e.g. @haskell_library(name = ..., ...)@.
data Call = Call
  { callFn :: String
  , callArgs :: [(String, Value)]
  }

call :: String -> [(String, Value)] -> Call
call = Call

indent :: Int -> String
indent n = replicate (4 * n) ' '

-- | Render like Python's @repr@ for a single-quoted string: this is what
-- both buck2/gen-haskell-prebuilt.py's generated output and every
-- hand-written @.bzl@ file in buck2/ already use.
renderStr :: String -> String
renderStr s = '\'' : concatMap escape s ++ "'"
  where
    escape '\\' = "\\\\"
    escape '\'' = "\\'"
    escape '\n' = "\\n"
    escape '\t' = "\\t"
    escape c = [c]

renderValue :: Int -> Value -> String
renderValue _ (VStr s) = renderStr s
renderValue _ (VInt n) = show n
renderValue _ (VBool b) = if b then "True" else "False"
renderValue _ (VList []) = "[]"
renderValue ind (VList xs) =
  "[\n"
    ++ concat [indent (ind + 1) ++ renderValue (ind + 1) x ++ ",\n" | x <- xs]
    ++ indent ind
    ++ "]"
renderValue _ (VDict []) = "{}"
renderValue ind (VDict kvs) =
  "{\n"
    ++ concat
      [ indent (ind + 1) ++ renderStr k ++ ": " ++ renderValue (ind + 1) v ++ ",\n"
      | (k, v) <- kvs
      ]
    ++ indent ind
    ++ "}"

-- | Render a rule call as a standalone top-level statement, one argument
-- per line (matching the style buck2/gen-haskell-prebuilt.py's own
-- generated BUCK file uses), terminated by a blank line.
renderCall :: Call -> String
renderCall (Call fn args) =
  fn
    ++ "(\n"
    ++ concat
      [ indent 1 ++ k ++ " = " ++ renderValue 1 v ++ ",\n"
      | (k, v) <- args
      ]
    ++ ")\n"

-- | Render a top-level @NAME = value@ statement.
renderBinding :: String -> Value -> String
renderBinding name v = name ++ " = " ++ renderValue 0 v ++ "\n"

-- | Render a @load("target", "name1", "name2")@ statement.
renderLoad :: String -> [String] -> String
renderLoad target names =
  "load(" ++ intercalate ", " (renderStr target : map renderStr names) ++ ")\n"

-- | Render a full generated file: a header comment, the accumulated
-- @load@ statements, then each rule call in turn, separated by blank
-- lines.
renderFile :: String -> [(String, [String])] -> [Call] -> String
renderFile header loads calls =
  unlines (map ("# " ++) headerLines)
    ++ "\n"
    ++ concatMap (uncurry renderLoad) loads
    ++ "\n"
    ++ intercalate "\n" (map renderCall calls)
  where
    headerLines = lines header