packages feed

nova-nix-0.9.0.0: src/Nix/Eval/Compile.hs

{-# LANGUAGE PatternSynonyms #-}

-- | Compile Nix 'Expr' AST trees to flat C bytecode.
--
-- Post-order traversal: children are compiled before parents, so child
-- bytecode indices are always less than parent indices.  Variable-length
-- data (string parts, bindings, formals, capture info, attr paths) goes
-- into a separate @uint32_t[]@ data buffer, referenced by offset from
-- instructions.
--
-- Must be called after 'cbcInit' and 'symbolInit'.
module Nix.Eval.Compile
  ( compileExpr,
    compileFormalsToEval,
    decodeBcFormals,
    decodeBcCaptureInfo,
    BcBinding (..),
    BcInheritedName (..),
    BcAttrKey (..),
    decodeBcBindings,
    reassembleInt64,
    reassembleDouble,
  )
where

import Data.Bits (shiftL, shiftR, (.&.), (.|.))
import Data.Int (Int64)
import Data.Text (Text)
import Data.Word (Word16, Word32, Word64, Word8)
import GHC.Float (castDoubleToWord64, castWord64ToDouble)
import Nix.Eval.CBytecode
  ( appDirect,
    attrkeyDynamic,
    attrkeyStatic,
    binaryAdd,
    binaryAnd,
    binaryConcat,
    binaryDiv,
    binaryEq,
    binaryGt,
    binaryGte,
    binaryImpl,
    binaryLt,
    binaryLte,
    binaryMul,
    binaryNeq,
    binaryOr,
    binarySub,
    binaryUpdate,
    bindInherit,
    bindInheritFrom,
    bindNamed,
    captureNone,
    captureSlots,
    captureWithScopes,
    cbcData,
    cbcEmit,
    cbcEmitData,
    formalName,
    formalNamedSet,
    formalSet,
    spilledCountSentinel,
    strpartEsc,
    strpartInterp,
    strpartLit,
    unaryNegate,
    unaryNot,
    pattern OpApp,
    pattern OpAssert,
    pattern OpAttrs,
    pattern OpBinary,
    pattern OpHasAttr,
    pattern OpIf,
    pattern OpIndStr,
    pattern OpLambda,
    pattern OpLet,
    pattern OpList,
    pattern OpLitBool,
    pattern OpLitFloat,
    pattern OpLitInt,
    pattern OpLitNull,
    pattern OpLitPath,
    pattern OpLitUri,
    pattern OpPathStr,
    pattern OpResolvedVar,
    pattern OpSearchPath,
    pattern OpSelect,
    pattern OpStr,
    pattern OpUnary,
    pattern OpVar,
    pattern OpWith,
    pattern OpWithVar,
  )
import Nix.Eval.EvalFormals (EvalFormal (..), EvalFormals (..))
import Nix.Eval.Symbol (Symbol (..), symbolIntern, symbolText)
import Nix.Expr.Types
  ( AttrKey (..),
    BinaryOp (..),
    Binding (..),
    CaptureInfo (..),
    Expr (..),
    Formal (..),
    Formals (..),
    NixAtom (..),
    StringPart (..),
    UnaryOp (..),
  )

-- | Compile an 'Expr' tree to bytecode, returning the root instruction index.
-- The bytecode is stored in the global @nn_bytecode@ arena.
compileExpr :: Expr -> IO Word32
compileExpr = go
  where
    go :: Expr -> IO Word32
    go (ELit atom) = compileLit atom
    go (EStr parts) = compileStringParts OpStr parts
    go (EIndStr parts) = compileStringParts OpIndStr parts
    go (EPathStr parts) = compileStringParts OpPathStr parts
    go (EVar name) = compileSymbolOp OpVar name
    go (EWithVar name) = compileSymbolOp OpWithVar name
    go (EResolvedVar level idx) =
      cbcEmit OpResolvedVar 0 0 (fi level) (fi idx) 0
    go (EAttrs isRec bindings captureInfo) =
      compileAttrs isRec bindings captureInfo
    go (EList exprs) = compileList exprs
    go (ESelect target path defExpr) =
      compileSelect target path defExpr
    go (EHasAttr target path) = compileHasAttr target path
    go (EApp func arg) = compileApp func arg
    go (ELambda formals body captureInfo) =
      compileLambda formals body captureInfo
    go (ELet bindings body captureInfo) =
      compileLet bindings body captureInfo
    go (EIf cond thenExpr elseExpr) = do
      condIdx <- go cond
      thenIdx <- go thenExpr
      elseIdx <- go elseExpr
      cbcEmit OpIf 0 0 condIdx thenIdx elseIdx
    go (EWith scope body) = do
      scopeIdx <- go scope
      bodyIdx <- go body
      cbcEmit OpWith 0 0 scopeIdx bodyIdx 0
    go (EAssert cond body) = do
      condIdx <- go cond
      bodyIdx <- go body
      cbcEmit OpAssert 0 0 condIdx bodyIdx 0
    go (EUnary op operand) = do
      operandIdx <- go operand
      cbcEmit OpUnary (encodeUnaryOp op) 0 operandIdx 0 0
    go (EBinary op left right) = do
      leftIdx <- go left
      rightIdx <- go right
      cbcEmit OpBinary (encodeBinaryOp op) 0 leftIdx rightIdx 0
    go (ESearchPath name) = compileSymbolOp OpSearchPath name

    -- -----------------------------------------------------------------
    -- Literals
    -- -----------------------------------------------------------------

    compileLit :: NixAtom -> IO Word32
    compileLit (NixInt n) =
      let (lo, hi) = splitInt64 n
       in cbcEmit OpLitInt 0 0 lo hi 0
    compileLit (NixFloat d) =
      let (lo, hi) = splitDouble d
       in cbcEmit OpLitFloat 0 0 lo hi 0
    compileLit (NixBool b) =
      cbcEmit OpLitBool 0 (if b then 1 else 0) 0 0 0
    compileLit NixNull =
      cbcEmit OpLitNull 0 0 0 0 0
    compileLit (NixUri u) = compileSymbolOp OpLitUri u
    compileLit (NixPath p) = compileSymbolOp OpLitPath p

    -- \| Emit an instruction whose only operand is an interned symbol.
    compileSymbolOp :: Word8 -> Text -> IO Word32
    compileSymbolOp op name = do
      Symbol sym <- symbolIntern name
      cbcEmit op 0 0 sym 0 0

    -- -----------------------------------------------------------------
    -- Applications (EApp)
    -- -----------------------------------------------------------------

    compileApp :: Expr -> Expr -> IO Word32
    compileApp func arg = do
      funcIdx <- go func
      argIdx <- go arg
      cbcEmit OpApp appDirect 0 funcIdx argIdx 0

    -- -----------------------------------------------------------------
    -- Strings (EStr / EIndStr)
    -- -----------------------------------------------------------------

    compileStringParts :: Word8 -> [StringPart] -> IO Word32
    compileStringParts op parts = do
      compiled <- mapM compileOnePart parts
      (partCount, dataOff) <- emitCounted "string with parts" (length parts) (pairWords compiled)
      cbcEmit op 0 partCount dataOff 0 0

    compileOnePart :: StringPart -> IO (Word32, Word32)
    compileOnePart (StrLit t) = do
      Symbol sym <- symbolIntern t
      pure (strpartLit, sym)
    compileOnePart (StrEsc t) = do
      Symbol sym <- symbolIntern t
      pure (strpartEsc, sym)
    compileOnePart (StrInterp expr) = do
      idx <- go expr
      pure (strpartInterp, idx)

    -- -----------------------------------------------------------------
    -- Attribute sets (EAttrs)
    -- -----------------------------------------------------------------

    compileAttrs :: Bool -> [Binding] -> CaptureInfo -> IO Word32
    compileAttrs isRec bindings captureInfo = do
      bindingWords <- compileBindingWords bindings
      capOff <- compileCaptureInfo captureInfo
      (bindingCount, dataOff) <- emitCounted "attribute set with bindings" (length bindings) bindingWords
      cbcEmit OpAttrs (if isRec then 1 else 0) bindingCount dataOff capOff 0

    -- -----------------------------------------------------------------
    -- Lists (EList)
    -- -----------------------------------------------------------------

    compileList :: [Expr] -> IO Word32
    compileList exprs = do
      childIndices <- mapM go exprs
      (elemCount, dataOff) <- emitCounted "list with elements" (length exprs) childIndices
      cbcEmit OpList 0 elemCount dataOff 0 0

    -- -----------------------------------------------------------------
    -- Select / HasAttr
    -- -----------------------------------------------------------------

    compileSelect :: Expr -> [AttrKey] -> Maybe Expr -> IO Word32
    compileSelect target path defExpr = do
      targetIdx <- go target
      defIdx <- case defExpr of
        Nothing -> pure 0
        Just d -> go d
      (pathOff, pathLen) <- compileAttrPath path
      let hasDef = case defExpr of Nothing -> 0; Just _ -> 1
      cbcEmit OpSelect hasDef pathLen targetIdx pathOff defIdx

    compileHasAttr :: Expr -> [AttrKey] -> IO Word32
    compileHasAttr target path = do
      targetIdx <- go target
      (pathOff, pathLen) <- compileAttrPath path
      cbcEmit OpHasAttr 0 pathLen targetIdx pathOff 0

    compileAttrPath :: [AttrKey] -> IO (Word32, Word16)
    compileAttrPath path = do
      compiled <- mapM compileAttrKey path
      (pathLen, off) <- emitCounted "attribute path with segments" (length path) (pairWords compiled)
      pure (off, pathLen)

    compileAttrKey :: AttrKey -> IO (Word32, Word32)
    compileAttrKey (StaticKey name) = do
      Symbol sym <- symbolIntern name
      pure (attrkeyStatic, sym)
    compileAttrKey (DynamicKey expr) = do
      idx <- go expr
      pure (attrkeyDynamic, idx)

    -- -----------------------------------------------------------------
    -- Lambda (ELambda)
    -- -----------------------------------------------------------------

    compileLambda :: Formals -> Expr -> CaptureInfo -> IO Word32
    compileLambda formals body captureInfo = do
      bodyIdx <- go body
      formalsOff <- compileFormals formals
      capOff <- compileCaptureInfo captureInfo
      cbcEmit OpLambda (encodeFormalType formals) 0 formalsOff bodyIdx capOff

    compileFormals :: Formals -> IO Word32
    compileFormals (FormalName name) = do
      Symbol sym <- symbolIntern name
      emitWordList [sym]
    compileFormals (FormalSet formals ellipsis) = do
      formalWords <- compileFormalEntries formals
      emitWordList $
        [fi (length formals), if ellipsis then 1 else 0]
          ++ formalWords
    compileFormals (FormalNamedSet name formals ellipsis) = do
      Symbol nameSym <- symbolIntern name
      formalWords <- compileFormalEntries formals
      emitWordList $
        [nameSym, fi (length formals), if ellipsis then 1 else 0]
          ++ formalWords

    compileFormalEntries :: [Formal] -> IO [Word32]
    compileFormalEntries = fmap concat . mapM compileFormalEntry

    compileFormalEntry :: Formal -> IO [Word32]
    compileFormalEntry (Formal name defExpr) = do
      Symbol nameSym <- symbolIntern name
      case defExpr of
        Nothing -> pure [nameSym, 0, 0]
        Just d -> do
          defIdx <- go d
          pure [nameSym, 1, defIdx]

    -- -----------------------------------------------------------------
    -- Let (ELet)
    -- -----------------------------------------------------------------

    compileLet :: [Binding] -> Expr -> CaptureInfo -> IO Word32
    compileLet bindings body captureInfo = do
      bodyIdx <- go body
      bindingWords <- compileBindingWords bindings
      capOff <- compileCaptureInfo captureInfo
      (bindingCount, dataOff) <- emitCounted "let with bindings" (length bindings) bindingWords
      cbcEmit OpLet 0 bindingCount dataOff bodyIdx capOff

    -- -----------------------------------------------------------------
    -- Bindings (shared by EAttrs and ELet)
    -- -----------------------------------------------------------------

    -- \| The flat data words for a binding list; the caller emits them
    -- (with the count, via emitCounted) so a spilled count can precede
    -- them contiguously.
    compileBindingWords :: [Binding] -> IO [Word32]
    compileBindingWords bindings = concat <$> mapM compileOneBinding bindings

    compileOneBinding :: Binding -> IO [Word32]
    compileOneBinding (NamedBinding key expr) = do
      (keyKind, keyWord) <- compileAttrKey key
      valIdx <- go expr
      pure [bindNamed, keyKind, keyWord, valIdx]
    compileOneBinding (Inherit name var) = do
      sym <- internName name
      valIdx <- go var
      pure [bindInherit, sym, valIdx]
    compileOneBinding (InheritFrom fromExpr names) = do
      fromIdx <- go fromExpr
      inherited <- mapM compileInheritedName names
      pure $
        [bindInheritFrom, fromIdx, fi (length names)]
          ++ concat inherited

    -- \| One name of @inherit (from) names@: its symbol, then the select
    -- that reads it from slot 0 of the env the evaluator puts the
    -- from-value in.  Compiled here with the binding, as upstream's parser
    -- makes an @ExprSelect@ per name, rather than each time the binding
    -- is evaluated, which would add a copy to a store that only grows.
    compileInheritedName :: Text -> IO [Word32]
    compileInheritedName name = do
      sym <- internName name
      selectIdx <- go (ESelect (EResolvedVar 0 0) [StaticKey name] Nothing)
      pure [sym, selectIdx]

    internName :: Text -> IO Word32
    internName name = do
      Symbol sym <- symbolIntern name
      pure sym

    -- -----------------------------------------------------------------
    -- CaptureInfo (shared by EAttrs, ELet, ELambda)
    -- -----------------------------------------------------------------

    compileCaptureInfo :: CaptureInfo -> IO Word32
    compileCaptureInfo NoCaptureInfo =
      emitWordList [captureNone]
    compileCaptureInfo (Captures pairs) =
      emitWordList $
        [captureSlots, fi (length pairs)]
          ++ concatMap (\(level, idx) -> [fi level, fi idx]) pairs
    compileCaptureInfo (CapturesWithScopes pairs) =
      emitWordList $
        [captureWithScopes, fi (length pairs)]
          ++ concatMap (\(level, idx) -> [fi level, fi idx]) pairs

    -- -----------------------------------------------------------------
    -- Operator encoding
    -- -----------------------------------------------------------------

    encodeUnaryOp :: UnaryOp -> Word8
    encodeUnaryOp OpNot = unaryNot
    encodeUnaryOp OpNegate = unaryNegate

    encodeBinaryOp :: BinaryOp -> Word8
    encodeBinaryOp OpAdd = binaryAdd
    encodeBinaryOp OpSub = binarySub
    encodeBinaryOp OpMul = binaryMul
    encodeBinaryOp OpDiv = binaryDiv
    encodeBinaryOp OpAnd = binaryAnd
    encodeBinaryOp OpOr = binaryOr
    encodeBinaryOp OpImpl = binaryImpl
    encodeBinaryOp OpEq = binaryEq
    encodeBinaryOp OpNeq = binaryNeq
    encodeBinaryOp OpLt = binaryLt
    encodeBinaryOp OpLte = binaryLte
    encodeBinaryOp OpGt = binaryGt
    encodeBinaryOp OpGte = binaryGte
    encodeBinaryOp OpConcat = binaryConcat
    encodeBinaryOp OpUpdate = binaryUpdate

    encodeFormalType :: Formals -> Word8
    encodeFormalType (FormalName {}) = formalName
    encodeFormalType (FormalSet {}) = formalSet
    encodeFormalType (FormalNamedSet {}) = formalNamedSet

    -- -----------------------------------------------------------------
    -- Numeric encoding
    -- -----------------------------------------------------------------

    splitInt64 :: Int64 -> (Word32, Word32)
    splitInt64 n =
      let w64 = fromIntegral n :: Word64
       in (fromIntegral (w64 .&. 0xFFFFFFFF), fromIntegral (shiftR w64 32))

    splitDouble :: Double -> (Word32, Word32)
    splitDouble d =
      let w64 = castDoubleToWord64 d
       in (fromIntegral (w64 .&. 0xFFFFFFFF), fromIntegral (shiftR w64 32))

    -- -----------------------------------------------------------------
    -- Data buffer helpers
    -- -----------------------------------------------------------------

    -- \| Flatten (tag, value) pairs into data-buffer words.
    pairWords :: [(Word32, Word32)] -> [Word32]
    pairWords = concatMap (\(tag, val) -> [tag, val])

    -- \| Emit a list of uint32 values to the data buffer.
    -- Returns the offset of the first emitted word, or 0 if empty.
    emitWordList :: [Word32] -> IO Word32
    emitWordList [] = pure 0
    emitWordList (x : xs) = do
      off <- cbcEmitData x
      mapM_ cbcEmitData xs
      pure off

    -- \| Emit an op's counted payload, returning the short_arg and data
    -- offset to store in the op.  A count below 'spilledCountSentinel'
    -- travels inline in short_arg; a larger one is written as the first
    -- data word with the sentinel inline ('cbcCountedPayload' is the
    -- decode side).  nn_op_t stays 16 bytes either way; only an
    -- oversized literal pays the extra word.
    emitCounted :: String -> Int -> [Word32] -> IO (Word16, Word32)
    emitCounted what n payload
      | n < fromIntegral spilledCountSentinel = do
          off <- emitWordList payload
          pure (fromIntegral n, off)
      | otherwise = do
          spilled <- countArg what n
          off <- emitWordList (spilled : payload)
          pure (spilledCountSentinel, off)

    -- \| Convert a spilled element count into its 32-bit data word,
    -- failing loudly at the (unreachable-in-practice) ceiling: a silent
    -- truncation would make an oversized literal report a wrong length.
    countArg :: String -> Int -> IO Word32
    countArg what n
      | toInteger n > toInteger (maxBound :: Word32) =
          ioError (userError ("compile: " <> what <> " exceeding the bytecode limit of 4294967295 (got " <> show n <> ")"))
      | otherwise = pure (fromIntegral n)

    -- \| @fromIntegral@ shorthand.
    fi :: (Integral a, Num b) => a -> b
    fi = fromIntegral

-- ---------------------------------------------------------------------------
-- Compile parser Formals to eval EvalFormals
-- ---------------------------------------------------------------------------

-- | Compile parser 'Formals' (with 'Maybe Expr' defaults) to eval-time
-- 'EvalFormals' (with 'Maybe Word32' bc_idx defaults).
compileFormalsToEval :: Formals -> IO EvalFormals
compileFormalsToEval (FormalName name) = pure (EFName name)
compileFormalsToEval (FormalSet formals ellipsis) =
  EFSet <$> mapM compileOneFormal formals <*> pure ellipsis
compileFormalsToEval (FormalNamedSet name formals ellipsis) =
  EFNamedSet name <$> mapM compileOneFormal formals <*> pure ellipsis

compileOneFormal :: Formal -> IO EvalFormal
compileOneFormal (Formal name defExpr) = do
  defBcIdx <- case defExpr of
    Nothing -> pure Nothing
    Just expr -> Just <$> compileExpr expr
  pure (EvalFormal name defBcIdx)

-- ---------------------------------------------------------------------------
-- Bytecode decoding (eval-time: read from C data buffer)
-- ---------------------------------------------------------------------------

-- | Decode formals from the bytecode data buffer.
-- @flags@ is the formal type (0=Name, 1=Set, 2=NamedSet).
-- @dataOff@ is the offset into the data buffer.
decodeBcFormals :: Word8 -> Word32 -> IO EvalFormals
decodeBcFormals flags dataOff = case flags of
  0 {- FormalName -} -> do
    sym <- cbcData dataOff
    pure (EFName (symbolText (Symbol sym)))
  1 {- FormalSet -} -> do
    count <- cbcData dataOff
    ellipsis <- cbcData (dataOff + 1)
    formals <- decodeFormalEntries (fromIntegral count) (dataOff + 2)
    pure (EFSet formals (ellipsis /= 0))
  2 {- FormalNamedSet -} -> do
    nameSym <- cbcData dataOff
    count <- cbcData (dataOff + 1)
    ellipsis <- cbcData (dataOff + 2)
    formals <- decodeFormalEntries (fromIntegral count) (dataOff + 3)
    pure (EFNamedSet (symbolText (Symbol nameSym)) formals (ellipsis /= 0))
  -- Unreachable: the flag is written only by this module's formals compiler,
  -- which emits 0, 1, or 2 - all matched above.
  _ -> error "decodeBcFormals: invalid formal type flag"

-- | Decode a list of formal entries from the data buffer.
-- Each entry is 3 words: [name_sym, has_default, default_bc_idx].
decodeFormalEntries :: Int -> Word32 -> IO [EvalFormal]
decodeFormalEntries 0 _ = pure []
decodeFormalEntries n off = do
  nameSym <- cbcData off
  hasDef <- cbcData (off + 1)
  defBcIdx <- cbcData (off + 2)
  let defMaybe = if hasDef /= 0 then Just defBcIdx else Nothing
  rest <- decodeFormalEntries (n - 1) (off + 3)
  pure (EvalFormal (symbolText (Symbol nameSym)) defMaybe : rest)

-- | Decode capture info from the bytecode data buffer.
decodeBcCaptureInfo :: Word32 -> IO CaptureInfo
decodeBcCaptureInfo dataOff = do
  tag <- cbcData dataOff
  case tag of
    0 {- NoCaptureInfo -} -> pure NoCaptureInfo
    1 {- Captures -} -> do
      count <- cbcData (dataOff + 1)
      pairs <- decodePairs (fromIntegral count) (dataOff + 2)
      pure (Captures pairs)
    2 {- CapturesWithScopes -} -> do
      count <- cbcData (dataOff + 1)
      pairs <- decodePairs (fromIntegral count) (dataOff + 2)
      pure (CapturesWithScopes pairs)
    -- Unreachable: the tag is written only by this module's capture compiler,
    -- which emits 0, 1, or 2 - all matched above.
    _ -> error "decodeBcCaptureInfo: invalid capture type tag"

-- | Decode (level, idx) pairs from the data buffer.
decodePairs :: Int -> Word32 -> IO [(Int, Int)]
decodePairs 0 _ = pure []
decodePairs n off = do
  level <- cbcData off
  idx <- cbcData (off + 1)
  rest <- decodePairs (n - 1) (off + 2)
  pure ((fromIntegral level, fromIntegral idx) : rest)

-- ---------------------------------------------------------------------------
-- Binding decoding (for evalBcAttrs / evalBcLet)
-- ---------------------------------------------------------------------------

-- | A decoded binding from the bytecode data buffer.
data BcBinding
  = -- | @key = expr@: the key and the value's bc_idx
    BcNamed !BcAttrKey !Word32
  | -- | One name of @inherit x@: its symbol and the bc_idx of the variable
    -- it copies, which is evaluated in the env around the binding set
    BcInherit !Word32 !Word32
  | -- | @inherit (from) names@ with from-expr bc_idx and the names
    BcInheritFrom !Word32 ![BcInheritedName]

-- | One name of an @inherit (from) names@ binding: its symbol and the
-- bc_idx of the select that reads it from slot 0 of an env holding the
-- from-value.
data BcInheritedName = BcInheritedName !Word32 !Word32

-- | A binding's attribute key.
data BcAttrKey
  = -- | Static key (interned symbol)
    BcStaticKey !Word32
  | -- | Dynamic key (bc_idx of expression to evaluate)
    BcDynamicKey !Word32

-- | Decode all bindings from the bytecode data buffer.
-- @bindCount@ is the number of bindings (already resolved through the
-- short_arg spill by the caller), @dataOff@ is the start offset.
decodeBcBindings :: Int -> Word32 -> IO [BcBinding]
decodeBcBindings 0 _ = pure []
decodeBcBindings count dataOff = do
  (binding, nextOff) <- decodeOneBinding dataOff
  rest <- decodeBcBindings (count - 1) nextOff
  pure (binding : rest)

-- | Decode a single binding, returning it and the offset past it.
decodeOneBinding :: Word32 -> IO (BcBinding, Word32)
decodeOneBinding off = do
  tag <- cbcData off
  case tag of
    0 {- NamedBinding -} -> do
      key <- decodeAttrKey (off + 1)
      valBcIdx <- cbcData (off + 3)
      pure (BcNamed key valBcIdx, off + 4)
    1 {- Inherit -} -> do
      sym <- cbcData (off + 1)
      valBcIdx <- cbcData (off + 2)
      pure (BcInherit sym valBcIdx, off + 3)
    2 {- InheritFrom -} -> do
      fromBcIdx <- cbcData (off + 1)
      nameCount <- cbcData (off + 2)
      names <- decodeInheritedNames (fromIntegral nameCount) (off + 3)
      pure (BcInheritFrom fromBcIdx names, off + 3 + 2 * nameCount)
    -- Unreachable: the tag is written only by this module's binding compiler,
    -- which emits 0, 1 or 2 - all matched above.
    _ -> error "decodeOneBinding: invalid binding type tag"

-- | Decode a binding's key: the pair (is_expr, key_or_bc_idx).
decodeAttrKey :: Word32 -> IO BcAttrKey
decodeAttrKey off = do
  isExpr <- cbcData off
  val <- cbcData (off + 1)
  pure (if isExpr /= 0 then BcDynamicKey val else BcStaticKey val)

-- | Decode inherited names: pairs of (name_sym, select_bc_idx).
decodeInheritedNames :: Int -> Word32 -> IO [BcInheritedName]
decodeInheritedNames 0 _ = pure []
decodeInheritedNames n off = do
  sym <- cbcData off
  selectBcIdx <- cbcData (off + 1)
  rest <- decodeInheritedNames (n - 1) (off + 2)
  pure (BcInheritedName sym selectBcIdx : rest)

-- ---------------------------------------------------------------------------
-- Numeric reassembly (from two uint32 halves)
-- ---------------------------------------------------------------------------

-- | Reassemble an Int64 from two uint32 halves (lo, hi).
reassembleInt64 :: Word32 -> Word32 -> Int64
reassembleInt64 lo hi =
  let w64 = fromIntegral lo .|. (fromIntegral hi `shiftL` 32) :: Word64
   in fromIntegral w64

-- | Reassemble a Double from two uint32 halves (lo, hi).
reassembleDouble :: Word32 -> Word32 -> Double
reassembleDouble lo hi =
  let w64 = fromIntegral lo .|. (fromIntegral hi `shiftL` 32) :: Word64
   in castWord64ToDouble w64