packages feed

tala-core-0.1.0: src/Progmasoft/Tala/Core/Wire.hs

-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1

{- | Versioned, bounded binary codec for the compiler's private Core IR.

Core artifacts are an internal stage boundary rather than a user-facing
emission format. Decoding applies explicit resource limits before exposing a
module to verification or native compilation.
-}
module Progmasoft.Tala.Core.Wire
    ( encodeCore
    , decodeCore
    , CoreWireVersion (..)
    , currentCoreWireVersion
    , CoreWireLimits (..)
    , defaultCoreWireLimits
    , CoreWireErrorKind (..)
    , CoreWireError (..)
    ) where

import Data.Bits (Bits, shiftL, shiftR, (.&.), (.|.))
import Data.Char (chr, ord)
import Data.Int (Int64)
import Data.List (unfoldr)
import Data.Map.Strict qualified as Map
import Data.Word (Word16, Word32, Word64, Word8)
import Progmasoft.Tala.AST
import Progmasoft.Tala.Core

-- | Unsigned schema version stored in a Core wire document header.
newtype CoreWireVersion = CoreWireVersion {coreWireVersionNumber :: Word16}
    deriving (Eq, Ord, Read, Show)

-- | Schema version emitted by the current Core writer.
currentCoreWireVersion :: CoreWireVersion
currentCoreWireVersion = CoreWireVersion 10

-- | Finite bounds for total bytes, recursion, and individual collections.
data CoreWireLimits = CoreWireLimits
    { maximumCoreWireBytes :: Int
    , maximumCoreTextScalars :: Int
    , maximumCoreFunctions :: Int
    , maximumCoreParameters :: Int
    , maximumCoreStatements :: Int
    , maximumCoreOperands :: Int
    , maximumCoreTypeDepth :: Int
    , maximumCoreExpressionDepth :: Int
    , maximumCoreNumericBytes :: Int
    }
    deriving (Eq, Ord, Read, Show)

-- | Default resource bounds shared by normal Core readers and writers.
defaultCoreWireLimits :: CoreWireLimits
defaultCoreWireLimits =
    CoreWireLimits
        { maximumCoreWireBytes = 64 * 1024 * 1024
        , maximumCoreTextScalars = 1024 * 1024
        , maximumCoreFunctions = 65535
        , maximumCoreParameters = 65535
        , maximumCoreStatements = 1048576
        , maximumCoreOperands = 65535
        , maximumCoreTypeDepth = 128
        , maximumCoreExpressionDepth = 4096
        , maximumCoreNumericBytes = 4096
        }

-- | Category of malformed input, unsupported schema data, or exceeded limit.
data CoreWireErrorKind
    = CoreInvalidMagic
    | CoreUnsupportedVersion
    | CoreTruncatedInput
    | CoreTrailingInput
    | CoreInvalidTag
    | CoreInvalidBoolean
    | CoreInvalidScalar
    | CoreInvalidCount
    | CoreInvalidSymbol
    | CoreInvalidInteger
    | CoreUnsupportedType
    | CoreLimitExceeded
    deriving (Eq, Ord, Read, Show)

-- | Structured wire error with byte offset and nested field context.
data CoreWireError = CoreWireError
    { coreWireErrorKind :: CoreWireErrorKind
    , coreWireErrorOffset :: Int
    , coreWireErrorContext :: String
    , coreWireErrorMessage :: String
    }
    deriving (Eq, Ord, Read, Show)

{- | Bytes under construction.

An encoded module is assembled from the encodings of its parts, and a part
may hold nearly all of the module: the body of a deeply nested statement, or
the rest of an @else if@ chain. Appending lists would copy such a part once
for every level around it, which makes encoding quadratic in the nesting
depth. A function that prepends its bytes to whatever follows is composed in
constant time, and the list is produced once, at the end.
-}
newtype Bytes = Bytes ([Word8] -> [Word8])

instance Semigroup Bytes where
    Bytes first <> Bytes second = Bytes (first . second)

instance Monoid Bytes where
    mempty = Bytes id

tagByte :: Word8 -> Bytes
tagByte value = Bytes (value :)

rawBytes :: [Word8] -> Bytes
rawBytes values = Bytes (values ++)

runBytes :: Bytes -> [Word8]
runBytes (Bytes prepend) = prepend []

type Encoder = Either CoreWireError Bytes

-- | Encode one Core module under explicit resource limits.
encodeCore :: CoreWireLimits -> CoreModule -> Either CoreWireError [Word8]
encodeCore limits moduleValue = do
    payload <- encodeModule limits moduleValue
    let output =
            runBytes
                (rawBytes magic <> rawBytes (word16 (coreWireVersionNumber currentCoreWireVersion)) <> rawBytes (word16 0) <> payload)
    requireEncode limits "wire byte length" (maximumCoreWireBytes limits) (length output)
    pure output

encodeModule :: CoreWireLimits -> CoreModule -> Encoder
encodeModule limits moduleValue = do
    name <- encodeQualifiedName limits (coreModuleName moduleValue)
    sources <-
        encodeVector
            limits
            "source file count"
            (maximumCoreFunctions limits)
            (encodeText limits "source file")
            (coreModuleSourceFiles moduleValue)
    let owners = Map.fromList (coreModuleFunctionSources moduleValue)
    functions <-
        encodeVector
            limits
            "function count"
            (maximumCoreFunctions limits)
            (encodeFunction limits owners)
            (coreModuleFunctions moduleValue)
    pure (name <> sources <> functions)

encodeFunction :: CoreWireLimits -> Map.Map Int FilePath -> CoreFunction -> Encoder
encodeFunction limits owners function = do
    name <- encodeResolvedName limits "function symbol" (coreFunctionName function)
    source <-
        encodeText
            limits
            "function source file"
            (Map.findWithDefault "" (symbolIdValue (resolvedSymbol (coreFunctionName function))) owners)
    parameters <-
        encodeVector
            limits
            "parameter count"
            (maximumCoreParameters limits)
            (encodeParameter limits)
            (coreFunctionParameters function)
    result <- encodeType limits 0 (coreFunctionReturnType function)
    body <-
        encodeVector
            limits
            "statement count"
            (maximumCoreStatements limits)
            (encodeStatement limits)
            (coreFunctionBody function)
    pure (name <> source <> parameters <> result <> body)

encodeParameter :: CoreWireLimits -> (ResolvedName, Type) -> Encoder
encodeParameter limits (name, valueType) =
    (<>)
        <$> encodeResolvedName limits "parameter symbol" name
        <*> encodeType limits 0 valueType

encodeStatement :: CoreWireLimits -> CoreStatement -> Encoder
encodeStatement limits statement = case statement of
    CoreBind binding -> do
        name <- encodeResolvedName limits "binding symbol" (coreBindingName binding)
        valueType <- encodeType limits 0 (coreBindingType binding)
        value <- encodeExpression limits 0 (coreBindingValue binding)
        pure (tagByte 0 <> name <> valueType <> rawBytes (encodeBool (coreBindingMutable binding)) <> value)
    CoreAssign name expression ->
        taggedExpression 1
            <$> encodeResolvedName limits "assignment symbol" name
            <*> encodeExpression limits 0 expression
    CoreReturn expression -> (tagByte 2 <>) <$> encodeExpression limits 0 expression
    CoreIf condition trueBranch falseBranch -> do
        encodedCondition <- encodeExpression limits 0 condition
        encodedTrue <-
            encodeVector
                limits
                "true branch statement count"
                (maximumCoreStatements limits)
                (encodeStatement limits)
                trueBranch
        encodedFalse <-
            encodeVector
                limits
                "false branch statement count"
                (maximumCoreStatements limits)
                (encodeStatement limits)
                falseBranch
        pure (tagByte 3 <> encodedCondition <> encodedTrue <> encodedFalse)
    CoreEvaluate expression -> (tagByte 4 <>) <$> encodeExpression limits 0 expression
    CoreWhile condition body -> do
        encodedCondition <- encodeExpression limits 0 condition
        encodedBody <-
            encodeVector
                limits
                "while body statement count"
                (maximumCoreStatements limits)
                (encodeStatement limits)
                body
        pure (tagByte 5 <> encodedCondition <> encodedBody)
    CoreDoWhile body condition -> do
        encodedBody <-
            encodeVector
                limits
                "do/while body statement count"
                (maximumCoreStatements limits)
                (encodeStatement limits)
                body
        encodedCondition <- encodeExpression limits 0 condition
        pure (tagByte 6 <> encodedBody <> encodedCondition)
    CoreFor condition body update -> do
        encodedCondition <- encodeExpression limits 0 condition
        encodedBody <-
            encodeVector
                limits
                "for body statement count"
                (maximumCoreStatements limits)
                (encodeStatement limits)
                body
        encodedUpdate <-
            encodeVector
                limits
                "for update statement count"
                (maximumCoreStatements limits)
                (encodeStatement limits)
                update
        pure (tagByte 7 <> encodedCondition <> encodedBody <> encodedUpdate)
    CoreBreak -> pure (tagByte 8)
    CoreContinue -> pure (tagByte 9)
    where
        taggedExpression tag left right = tagByte tag <> left <> right

encodeExpression :: CoreWireLimits -> Int -> CoreExpression -> Encoder
encodeExpression limits depth expression
    | depth > maximumCoreExpressionDepth limits = failure CoreLimitExceeded "expression" "expression nesting exceeds limit"
    | otherwise = case expression of
        CoreVariable name valueType -> do
            encodedType <- encodeType limits 0 valueType
            encodedName <- encodeResolvedName limits "variable symbol" name
            pure (tagByte 0 <> encodedType <> encodedName)
        CoreLiteral literal valueType -> do
            encodedType <- encodeType limits 0 valueType
            encodedLiteral <- encodeLiteral limits literal
            pure (tagByte 1 <> encodedType <> encodedLiteral)
        CoreApply callee arguments valueType -> do
            encodedType <- encodeType limits 0 valueType
            encodedCallee <- encodeExpression limits (depth + 1) callee
            encodedArguments <-
                encodeVector
                    limits
                    "call argument count"
                    (maximumCoreOperands limits)
                    (encodeExpression limits (depth + 1))
                    arguments
            pure (tagByte 2 <> encodedType <> encodedCallee <> encodedArguments)
        -- The first operand of a primitive is at the depth of the
        -- primitive: a chain of operators nests in it, as deep as the chain
        -- is long, and every stage walks that chain in a loop.
        CorePrimitive primitive arguments valueType -> do
            encodedType <- encodeType limits 0 valueType
            encodedArguments <-
                encodeVector
                    limits
                    "primitive operand count"
                    (maximumCoreOperands limits)
                    (\(position, argument) -> encodeExpression limits (depth + position) argument)
                    (zip (0 : repeat 1) arguments)
            pure (rawBytes [3, primitiveTag primitive] <> encodedType <> encodedArguments)
        CoreClosure captures parameters returnType body valueType -> do
            encodedType <- encodeType limits 0 valueType
            encodedCaptures <-
                encodeVector
                    limits
                    "closure capture count"
                    (maximumCoreOperands limits)
                    (encodeCoreCapture limits depth)
                    captures
            encodedParameters <-
                encodeVector
                    limits
                    "closure parameter count"
                    (maximumCoreParameters limits)
                    (encodeParameter limits)
                    parameters
            encodedReturn <- encodeType limits 0 returnType
            encodedBody <-
                encodeVector
                    limits
                    "closure statement count"
                    (maximumCoreStatements limits)
                    (encodeStatement limits)
                    body
            pure (tagByte 4 <> encodedType <> encodedCaptures <> encodedParameters <> encodedReturn <> encodedBody)
        CoreLet name bindingType value body valueType -> do
            encodedType <- encodeType limits 0 valueType
            encodedName <- encodeResolvedName limits "let symbol" name
            encodedBindingType <- encodeType limits 0 bindingType
            encodedValue <- encodeExpression limits (depth + 1) value
            encodedBody <- encodeExpression limits (depth + 1) body
            pure (tagByte 5 <> encodedType <> encodedName <> encodedBindingType <> encodedValue <> encodedBody)
        CoreConditional condition whenTrue whenFalse valueType -> do
            encodedType <- encodeType limits 0 valueType
            encodedCondition <- encodeExpression limits (depth + 1) condition
            encodedTrue <- encodeExpression limits (depth + 1) whenTrue
            encodedFalse <- encodeExpression limits (depth + 1) whenFalse
            pure (tagByte 6 <> encodedType <> encodedCondition <> encodedTrue <> encodedFalse)

encodeCoreCapture :: CoreWireLimits -> Int -> CoreCapture -> Encoder
encodeCoreCapture limits depth capture = do
    encodedName <- encodeResolvedName limits "closure capture symbol" (coreCaptureName capture)
    encodedType <- encodeType limits 0 (coreCaptureType capture)
    encodedValue <- encodeExpression limits (depth + 1) (coreCaptureValue capture)
    pure (tagByte (captureModeTag (coreCaptureMode capture)) <> encodedName <> encodedType <> encodedValue)

captureModeTag :: CaptureMode -> Word8
captureModeTag mode = case mode of
    StrongCapture -> 0
    WeakCapture -> 1
    UnownedCapture -> 2

encodeLiteral :: CoreWireLimits -> CoreLiteral -> Encoder
encodeLiteral limits literal = case literal of
    CoreUnit -> pure (tagByte 0)
    CoreBoolean value -> pure (tagByte 1 <> rawBytes (encodeBool value))
    CoreInteger value -> (tagByte 4 <>) <$> encodeInteger limits value
    CoreFloating spelling -> (tagByte 5 <>) <$> encodeAscii limits "floating literal" spelling
    CoreString value -> (tagByte 3 <>) <$> encodeText limits "string literal" value
    CoreNull -> pure (tagByte 6)

encodeType :: CoreWireLimits -> Int -> Type -> Encoder
encodeType limits depth valueType
    | depth > maximumCoreTypeDepth limits = failure CoreLimitExceeded "type" "type nesting exceeds limit"
    | valueType == unitType = pure (tagByte 0)
    | valueType == boolType = pure (tagByte 1)
    | valueType == intType = pure (tagByte 2)
    | valueType == stringType = pure (tagByte 3)
    | Just scalarTag <- scalarTypeTag valueType = pure (tagByte scalarTag)
    | NamedType name arguments <- valueType = do
        encodedName <- encodeQualifiedName limits name
        encodedArguments <-
            encodeVector
                limits
                "type argument count"
                (maximumCoreOperands limits)
                (encodeTemplateArgument limits (depth + 1))
                arguments
        pure (tagByte 4 <> encodedName <> encodedArguments)
    | FunctionType parameters result <- valueType = do
        encodedParameters <-
            encodeVector
                limits
                "function type parameter count"
                (maximumCoreParameters limits)
                (encodeType limits (depth + 1))
                parameters
        encodedResult <- encodeType limits (depth + 1) result
        pure (tagByte 5 <> encodedParameters <> encodedResult)
    | TypeVariable name <- valueType = (tagByte 6 <>) <$> encodeResolvedName limits "type variable symbol" name
    | ErrorType <- valueType = failure CoreUnsupportedType "type" "ErrorType cannot cross the Core boundary"

encodeTemplateArgument :: CoreWireLimits -> Int -> TemplateArgument -> Encoder
encodeTemplateArgument limits depth argument = case argument of
    TypeTemplateArgument valueType -> (tagByte 0 <>) <$> encodeType limits depth valueType
    ValueTemplateArgument value -> encodeTemplateValue limits value

encodeTemplateValue :: CoreWireLimits -> TemplateValue -> Encoder
encodeTemplateValue limits value = case value of
    IntegerTemplateValue integer -> (tagByte 1 <>) <$> encodeInteger limits integer
    BooleanTemplateValue boolean -> pure (rawBytes [2, if boolean then 1 else 0])
    CharacterTemplateValue scalar -> (tagByte 3 <>) <$> encodeInteger limits scalar
    TemplateValueParameter name -> (tagByte 4 <>) <$> encodeResolvedName limits "template value parameter" name

encodeResolvedName :: CoreWireLimits -> String -> ResolvedName -> Encoder
encodeResolvedName limits context name
    | symbolIdValue (resolvedSymbol name) <= 0 = failure CoreInvalidSymbol context "symbol id must be positive"
    | otherwise = do
        spelling <- encodeText limits (context ++ " spelling") (identifierText (resolvedSpelling name))
        pure (rawBytes (word64 (fromIntegral (symbolIdValue (resolvedSymbol name)))) <> spelling)

encodeQualifiedName :: CoreWireLimits -> QualifiedName -> Encoder
encodeQualifiedName limits (QualifiedName parts) =
    encodeVector
        limits
        "qualified name parts"
        65535
        (encodeText limits "qualified name part" . identifierText)
        parts

encodeVector :: CoreWireLimits -> String -> Int -> (a -> Encoder) -> [a] -> Encoder
encodeVector limits context maximumValue encode values = do
    requireEncode limits context maximumValue (length values)
    encoded <- traverse encode values
    pure (rawBytes (word32 (fromIntegral (length values))) <> mconcat encoded)

encodeText :: CoreWireLimits -> String -> String -> Encoder
encodeText limits context value = do
    requireEncode limits context (maximumCoreTextScalars limits) (length value)
    codePoints <- traverse encodeScalar value
    pure (rawBytes (word32 (fromIntegral (length value))) <> mconcat codePoints)
    where
        encodeScalar character
            | code >= 0xd800 && code <= 0xdfff = failure CoreInvalidScalar context "surrogate is not a Unicode scalar"
            | code > 0x10ffff = failure CoreInvalidScalar context "code point exceeds Unicode range"
            | otherwise = pure (rawBytes (word32 (fromIntegral code)))
            where
                code = ord character

scalarTypeTag :: Type -> Maybe Word8
scalarTypeTag (NamedType (QualifiedName [Identifier name]) []) = lookup name tags
    where
        tags =
            [ ("char", 7)
            , ("byte", 8)
            , ("short", 9)
            , ("long", 10)
            , ("longint", 11)
            , ("ubyte", 12)
            , ("ushort", 13)
            , ("ulong", 14)
            , ("uint", 15)
            , ("ulongint", 16)
            , ("sfloat", 17)
            , ("lfloat", 18)
            , ("float", 19)
            , ("double", 20)
            ]
scalarTypeTag _ = Nothing

encodeInteger :: CoreWireLimits -> Integer -> Encoder
encodeInteger limits value = do
    let magnitude = integerMagnitude (abs value)
    requireEncode limits "integer magnitude" (maximumCoreNumericBytes limits) (length magnitude)
    pure (rawBytes (encodeBool (value < 0)) <> rawBytes (word32 (fromIntegral (length magnitude))) <> rawBytes magnitude)

integerMagnitude :: Integer -> [Word8]
integerMagnitude 0 = []
integerMagnitude value = reverse (unfoldr step value)
    where
        step 0 = Nothing
        step remaining = Just (fromIntegral (remaining `mod` 256), remaining `div` 256)

encodeAscii :: CoreWireLimits -> String -> String -> Encoder
encodeAscii limits context value = do
    requireEncode limits context (maximumCoreNumericBytes limits) (length value)
    characters <- traverse ascii value
    pure (rawBytes (word32 (fromIntegral (length characters))) <> rawBytes characters)
    where
        ascii character
            | ord character <= 0x7f = Right (fromIntegral (ord character))
            | otherwise = failure CoreInvalidInteger context "numeric spelling must contain ASCII characters only"

requireEncode :: CoreWireLimits -> String -> Int -> Int -> Either CoreWireError ()
requireEncode _ context maximumValue actual
    | actual > maximumValue = failure CoreLimitExceeded context ("count exceeds configured limit: " ++ show actual)
    | otherwise = Right ()

data DecoderState = DecoderState [Word8] Int CoreWireLimits
newtype Decoder a = Decoder {runDecoder :: DecoderState -> Either CoreWireError (a, DecoderState)}

instance Functor Decoder where
    fmap transform parser = Decoder $ \state -> do
        (value, next) <- runDecoder parser state
        pure (transform value, next)
instance Applicative Decoder where
    pure value = Decoder $ \state -> Right (value, state)
    functionParser <*> valueParser = Decoder $ \state -> do
        (function, afterFunction) <- runDecoder functionParser state
        (value, afterValue) <- runDecoder valueParser afterFunction
        pure (function value, afterValue)
instance Monad Decoder where
    parser >>= nextParser = Decoder $ \state -> do
        (value, afterValue) <- runDecoder parser state
        runDecoder (nextParser value) afterValue

-- | Decode and validate one Core document under explicit resource limits.
decodeCore :: CoreWireLimits -> [Word8] -> Either CoreWireError CoreModule
decodeCore limits bytes
    | length bytes > maximumCoreWireBytes limits =
        failure CoreLimitExceeded "wire byte length" "input exceeds configured limit"
    | otherwise = do
        (moduleValue, DecoderState remaining offset _) <- runDecoder decodeDocument (DecoderState bytes 0 limits)
        if null remaining
            then Right moduleValue
            else Left (CoreWireError CoreTrailingInput offset "document" "bytes remain after Core module")

decodeDocument :: Decoder CoreModule
decodeDocument = do
    header <- takeBytes "magic" 4
    requireDecode (header == magic) CoreInvalidMagic "magic" "input is not a Tala Core document"
    version <- readWord16 "version"
    requireDecode
        (version == coreWireVersionNumber currentCoreWireVersion)
        CoreUnsupportedVersion
        "version"
        "unsupported Core wire version"
    flags <- readWord16 "flags"
    requireDecode (flags == 0) CoreInvalidTag "flags" "reserved flags must be zero"
    name <- decodeQualifiedName
    sources <- decodeVector "source file count" maximumCoreFunctions (decodeText "source file")
    functionsWithSources <- decodeVector "function count" maximumCoreFunctions decodeFunction
    let functions = map fst functionsWithSources
        owners =
            [ (symbolIdValue (resolvedSymbol (coreFunctionName function)), source)
            | (function, source) <- functionsWithSources
            , not (null source)
            ]
    pure (CoreModuleWithSources name functions sources owners)

decodeFunction :: Decoder (CoreFunction, FilePath)
decodeFunction = do
    name <- decodeResolvedName "function symbol"
    source <- decodeText "function source file"
    parameters <- decodeVector "parameter count" maximumCoreParameters decodeParameter
    result <- decodeType 0
    body <- decodeVector "statement count" maximumCoreStatements decodeStatement
    pure (CoreFunction name parameters result body, source)

decodeParameter :: Decoder (ResolvedName, Type)
decodeParameter = (,) <$> decodeResolvedName "parameter symbol" <*> decodeType 0

decodeStatement :: Decoder CoreStatement
decodeStatement = do
    tag <- readWord8 "statement tag"
    case tag of
        0 ->
            CoreBind
                <$> ( CoreBinding
                        <$> decodeResolvedName "binding symbol"
                        <*> decodeType 0
                        <*> decodeBool "binding mutability"
                        <*> decodeExpression 0
                    )
        1 -> CoreAssign <$> decodeResolvedName "assignment symbol" <*> decodeExpression 0
        2 -> CoreReturn <$> decodeExpression 0
        3 ->
            CoreIf
                <$> decodeExpression 0
                <*> decodeVector "true branch statement count" maximumCoreStatements decodeStatement
                <*> decodeVector "false branch statement count" maximumCoreStatements decodeStatement
        4 -> CoreEvaluate <$> decodeExpression 0
        5 -> CoreWhile <$> decodeExpression 0 <*> decodeVector "while body statement count" maximumCoreStatements decodeStatement
        6 ->
            CoreDoWhile
                <$> decodeVector "do/while body statement count" maximumCoreStatements decodeStatement
                <*> decodeExpression 0
        7 ->
            CoreFor
                <$> decodeExpression 0
                <*> decodeVector "for body statement count" maximumCoreStatements decodeStatement
                <*> decodeVector "for update statement count" maximumCoreStatements decodeStatement
        8 -> pure CoreBreak
        9 -> pure CoreContinue
        _ -> invalidTag "statement tag" tag

decodeExpression :: Int -> Decoder CoreExpression
decodeExpression depth = do
    limits <- currentLimits
    requireDecode
        (depth <= maximumCoreExpressionDepth limits)
        CoreLimitExceeded
        "expression"
        "expression nesting exceeds limit"
    tag <- readWord8 "expression tag"
    case tag of
        0 -> flip CoreVariable <$> decodeType 0 <*> decodeResolvedName "variable symbol"
        1 -> do valueType <- decodeType 0; literal <- decodeLiteral; pure (CoreLiteral literal valueType)
        2 -> do
            valueType <- decodeType 0
            callee <- decodeExpression (depth + 1)
            arguments <- decodeVector "call argument count" maximumCoreOperands (decodeExpression (depth + 1))
            pure (CoreApply callee arguments valueType)
        3 -> do
            primitive <- readWord8 "primitive tag" >>= decodePrimitive
            valueType <- decodeType 0
            count <- readWord32 "primitive operand count"
            requireDecode
                (toInteger count <= toInteger (maximumCoreOperands limits))
                CoreLimitExceeded
                "primitive operand count"
                "count exceeds configured limit"
            -- The first operand is at the depth of the primitive.
            arguments <-
                sequence
                    [ decodeExpression (depth + position)
                    | position <- take (fromIntegral count) (0 : repeat 1)
                    ]
            pure (CorePrimitive primitive arguments valueType)
        4 -> do
            valueType <- decodeType 0
            captures <- decodeVector "closure capture count" maximumCoreOperands (decodeCoreCapture (depth + 1))
            parameters <- decodeVector "closure parameter count" maximumCoreParameters decodeParameter
            returnType <- decodeType 0
            body <- decodeVector "closure statement count" maximumCoreStatements decodeStatement
            pure (CoreClosure captures parameters returnType body valueType)
        5 -> do
            valueType <- decodeType 0
            name <- decodeResolvedName "let symbol"
            bindingType <- decodeType 0
            value <- decodeExpression (depth + 1)
            body <- decodeExpression (depth + 1)
            pure (CoreLet name bindingType value body valueType)
        6 -> do
            valueType <- decodeType 0
            condition <- decodeExpression (depth + 1)
            whenTrue <- decodeExpression (depth + 1)
            whenFalse <- decodeExpression (depth + 1)
            pure (CoreConditional condition whenTrue whenFalse valueType)
        _ -> invalidTag "expression tag" tag

decodeCoreCapture :: Int -> Decoder CoreCapture
decodeCoreCapture depth = do
    mode <- readWord8 "closure capture mode" >>= decodeCaptureMode
    name <- decodeResolvedName "closure capture symbol"
    valueType <- decodeType 0
    value <- decodeExpression depth
    pure (CoreCapture mode name valueType value)

decodeCaptureMode :: Word8 -> Decoder CaptureMode
decodeCaptureMode tag = case tag of
    0 -> pure StrongCapture
    1 -> pure WeakCapture
    2 -> pure UnownedCapture
    _ -> invalidTag "closure capture mode" tag

decodeLiteral :: Decoder CoreLiteral
decodeLiteral = do
    tag <- readWord8 "literal tag"
    case tag of
        0 -> pure CoreUnit
        1 -> CoreBoolean <$> decodeBool "boolean literal"
        2 -> CoreInteger . fromIntegral . (fromIntegral :: Word64 -> Int64) <$> readWord64 "integer literal"
        3 -> CoreString <$> decodeText "string literal"
        4 -> CoreInteger <$> decodeInteger
        5 -> CoreFloating <$> decodeAscii "floating literal"
        6 -> pure CoreNull
        _ -> invalidTag "literal tag" tag

decodeType :: Int -> Decoder Type
decodeType depth = do
    limits <- currentLimits
    requireDecode (depth <= maximumCoreTypeDepth limits) CoreLimitExceeded "type" "type nesting exceeds limit"
    tag <- readWord8 "type tag"
    case tag of
        0 -> pure unitType
        1 -> pure boolType
        2 -> pure intType
        3 -> pure stringType
        4 ->
            NamedType
                <$> decodeQualifiedName
                <*> decodeVector "template argument count" maximumCoreOperands (decodeTemplateArgument (depth + 1))
        5 ->
            FunctionType
                <$> decodeVector "function type parameter count" maximumCoreParameters (decodeType (depth + 1))
                <*> decodeType (depth + 1)
        6 -> TypeVariable <$> decodeResolvedName "type variable symbol"
        7 -> pure (namedScalar "char")
        8 -> pure (namedScalar "byte")
        9 -> pure (namedScalar "short")
        10 -> pure (namedScalar "long")
        11 -> pure (namedScalar "longint")
        12 -> pure (namedScalar "ubyte")
        13 -> pure (namedScalar "ushort")
        14 -> pure (namedScalar "ulong")
        15 -> pure (namedScalar "uint")
        16 -> pure (namedScalar "ulongint")
        17 -> pure (namedScalar "sfloat")
        18 -> pure (namedScalar "lfloat")
        19 -> pure (namedScalar "float")
        20 -> pure (namedScalar "double")
        _ -> invalidTag "type tag" tag

decodeTemplateArgument :: Int -> Decoder TemplateArgument
decodeTemplateArgument depth = do
    tag <- readWord8 "template argument tag"
    case tag of
        0 -> TypeTemplateArgument <$> decodeType depth
        1 -> ValueTemplateArgument . IntegerTemplateValue <$> decodeInteger
        2 -> ValueTemplateArgument . BooleanTemplateValue <$> decodeBool "template boolean value"
        3 -> ValueTemplateArgument . CharacterTemplateValue <$> decodeInteger
        4 -> ValueTemplateArgument . TemplateValueParameter <$> decodeResolvedName "template value parameter"
        _ -> invalidTag "template argument tag" tag

namedScalar :: String -> Type
namedScalar name = NamedType (QualifiedName [Identifier name]) []

decodeResolvedName :: String -> Decoder ResolvedName
decodeResolvedName context = do
    raw <- readWord64 context
    requireDecode (raw > 0 && raw <= fromIntegral (maxBound :: Int)) CoreInvalidSymbol context "invalid symbol id"
    ResolvedName (SymbolId (fromIntegral raw)) . Identifier <$> decodeText (context ++ " spelling")

decodeQualifiedName :: Decoder QualifiedName
decodeQualifiedName = QualifiedName <$> decodeVectorMaximum "qualified name parts" 65535 (Identifier <$> decodeText "qualified name part")

decodeVector :: String -> (CoreWireLimits -> Int) -> Decoder a -> Decoder [a]
decodeVector context selectMaximum parser = currentLimits >>= \limits -> decodeVectorMaximum context (selectMaximum limits) parser

decodeVectorMaximum :: String -> Int -> Decoder a -> Decoder [a]
decodeVectorMaximum context maximumValue parser = do
    count <- readWord32 context
    requireDecode (toInteger count <= toInteger maximumValue) CoreLimitExceeded context "count exceeds configured limit"
    sequence (replicate (fromIntegral count) parser)

decodeText :: String -> Decoder String
decodeText context = do
    limits <- currentLimits
    count <- readWord32 (context ++ " length")
    requireDecode
        (toInteger count <= toInteger (maximumCoreTextScalars limits))
        CoreLimitExceeded
        context
        "text exceeds configured limit"
    sequence (replicate (fromIntegral count) decodeScalar)
    where
        decodeScalar = do
            value <- readWord32 context
            requireDecode
                (value <= 0x10ffff && not (value >= 0xd800 && value <= 0xdfff))
                CoreInvalidScalar
                context
                "invalid Unicode scalar"
            pure (chr (fromIntegral value))

decodeInteger :: Decoder Integer
decodeInteger = do
    negative <- decodeBool "integer sign"
    limits <- currentLimits
    count <- readWord32 "integer magnitude length"
    requireDecode
        (toInteger count <= toInteger (maximumCoreNumericBytes limits))
        CoreLimitExceeded
        "integer magnitude"
        "integer magnitude exceeds configured limit"
    magnitude <- takeBytes "integer magnitude" (fromIntegral count)
    requireDecode
        (canonicalMagnitude magnitude)
        CoreInvalidInteger
        "integer magnitude"
        "integer magnitude is not canonical"
    requireDecode
        (not negative || not (null magnitude))
        CoreInvalidInteger
        "integer sign"
        "zero cannot have a negative sign"
    let value = foldl (\result octet -> result * 256 + fromIntegral octet) 0 magnitude
    pure (if negative then negate value else value)
    where
        canonicalMagnitude [] = True
        canonicalMagnitude (first : _) = first /= 0

decodeAscii :: String -> Decoder String
decodeAscii context = do
    limits <- currentLimits
    count <- readWord32 (context ++ " length")
    requireDecode
        (toInteger count <= toInteger (maximumCoreNumericBytes limits))
        CoreLimitExceeded
        context
        "numeric spelling exceeds configured limit"
    bytes <- takeBytes context (fromIntegral count)
    requireDecode (all (<= 0x7f) bytes) CoreInvalidInteger context "numeric spelling must contain ASCII characters only"
    pure (map (chr . fromIntegral) bytes)

decodeBool :: String -> Decoder Bool
decodeBool context =
    readWord8 context >>= \value -> case value of
        0 -> pure False
        1 -> pure True
        _ -> failDecode CoreInvalidBoolean context "invalid boolean byte"

decodePrimitive :: Word8 -> Decoder CorePrimitive
decodePrimitive tag = case drop (fromIntegral tag) primitives of
    primitive : _ -> pure primitive
    [] -> invalidTag "primitive tag" tag
    where
        primitives =
            [ CoreAdd
            , CoreSubtract
            , CoreMultiply
            , CoreDivide
            , CoreFloorDivide
            , CoreRemainder
            , CoreLessThan
            , CoreLessEqual
            , CoreGreaterThan
            , CoreGreaterEqual
            , CoreEqual
            , CoreNotEqual
            , CoreLogicalAnd
            , CoreLogicalOr
            , CoreNegate
            , CoreLogicalNot
            , CorePower
            , CoreShiftLeft
            , CoreShiftRight
            , CoreBitwiseAnd
            , CoreBitwiseXor
            , CoreBitwiseOr
            , CoreBitwiseNot
            , CoreTypeIs
            , CoreMemoize
            , CoreRuntimeCall
            ]

primitiveTag :: CorePrimitive -> Word8
primitiveTag primitive = fromIntegral (index primitive primitives)
    where
        primitives =
            [ CoreAdd
            , CoreSubtract
            , CoreMultiply
            , CoreDivide
            , CoreFloorDivide
            , CoreRemainder
            , CoreLessThan
            , CoreLessEqual
            , CoreGreaterThan
            , CoreGreaterEqual
            , CoreEqual
            , CoreNotEqual
            , CoreLogicalAnd
            , CoreLogicalOr
            , CoreNegate
            , CoreLogicalNot
            , CorePower
            , CoreShiftLeft
            , CoreShiftRight
            , CoreBitwiseAnd
            , CoreBitwiseXor
            , CoreBitwiseOr
            , CoreBitwiseNot
            , CoreTypeIs
            , CoreMemoize
            , CoreRuntimeCall
            ]
        index :: CorePrimitive -> [CorePrimitive] -> Int
        index value (candidate : remaining) = if value == candidate then 0 else 1 + index value remaining
        index _ [] = error "CorePrimitive enumeration is incomplete"

currentLimits :: Decoder CoreWireLimits
currentLimits = Decoder $ \state@(DecoderState _ _ limits) -> Right (limits, state)

requireDecode :: Bool -> CoreWireErrorKind -> String -> String -> Decoder ()
requireDecode condition kind context message = if condition then pure () else failDecode kind context message

invalidTag :: String -> Word8 -> Decoder a
invalidTag context tag = failDecode CoreInvalidTag context ("unknown tag " ++ show tag)

failDecode :: CoreWireErrorKind -> String -> String -> Decoder a
failDecode kind context message = Decoder $ \(DecoderState _ offset _) -> Left (CoreWireError kind offset context message)

takeBytes :: String -> Int -> Decoder [Word8]
takeBytes context count = Decoder $ \(DecoderState bytes offset limits) ->
    let (prefix, suffix) = splitAt count bytes
     in if length prefix == count
            then Right (prefix, DecoderState suffix (offset + count) limits)
            else Left (CoreWireError CoreTruncatedInput offset context "input ended before field was complete")

readWord8 :: String -> Decoder Word8
readWord8 context = do
    bytes <- takeBytes context 1
    case bytes of
        [value] -> pure value
        _ -> failDecode CoreTruncatedInput context "byte reader returned no value"
readWord16 :: String -> Decoder Word16
readWord16 context = assemble <$> takeBytes context 2
readWord32 :: String -> Decoder Word32
readWord32 context = assemble <$> takeBytes context 4
readWord64 :: String -> Decoder Word64
readWord64 context = assemble <$> takeBytes context 8

magic :: [Word8]
magic = map (fromIntegral . fromEnum) "TACR"

failure :: CoreWireErrorKind -> String -> String -> Either CoreWireError a
failure kind context message = Left (CoreWireError kind 0 context message)

encodeBool :: Bool -> [Word8]
encodeBool False = [0]
encodeBool True = [1]
word16 :: Word16 -> [Word8]
word16 value = [byte value 0, byte value 8]
word32 :: Word32 -> [Word8]
word32 value = [byte value 0, byte value 8, byte value 16, byte value 24]
word64 :: Word64 -> [Word8]
word64 value =
    [byte value 0, byte value 8, byte value 16, byte value 24, byte value 32, byte value 40, byte value 48, byte value 56]
byte :: (Integral a, Bits a) => a -> Int -> Word8
byte value amount = fromIntegral ((value `shiftR` amount) .&. 0xff)
assemble :: (Integral a, Bits a) => [Word8] -> a
assemble bytes = foldr (\(position, value) result -> result .|. (fromIntegral value `shiftL` (position * 8))) 0 (zip [0 ..] bytes)