elm-cardano/bytes-decoder/1.1.0

NOTE: the latest version is 1.2.0

Bytes.Decoder

Fast bytes decoder with error reporting and oneOf branching.

Architecture

Each Decoder carries two execution strategies:

  1. Fast path — a raw elm/bytes Bytes.Decode.Decoder composed via Decode.map2, Decode.andThen, Decode.loop, etc. When present, decode executes a single Decode.decode call with zero per-step overhead. This matches the performance of hand-written elm/bytes decoders.

  2. Slow path — a state-passing function that tracks byte offsets, reports structured errors, and supports backtracking in oneOf. This path is only executed when the fast path is unavailable or returns Nothing (i.e. an error occurred).

The key insight is that most decoders succeed on well-formed input. By deferring error tracking to a re-decode, the happy path pays no cost for error reporting.

Fast-path availability

Not every combinator can be expressed as a raw Bytes.Decode.Decoder:

  • Always fast: succeed, map, map2–map5, keep, ignore, skip, andThen, loop, repeat, and all primitives.
  • Always slow: fail (no decoder can produce a value), oneOf (requires backtracking which Bytes.Decode.Decoder cannot do mid-stream).
  • Conditionally fast: andThen and loop remain fast as long as the callback returns a decoder with a fast path. If a callback branch returns fail, that branch uses Decode.fail to signal the raw decoder, which causes Decode.decode to return Nothing and triggers the slow re-decode.

In practice, fail branches inside andThen are error-handling code that only runs on malformed input — exactly when we want to re-decode with error tracking.

Running

type Decoder context error value

A decoder that reads binary data and produces either a value or an error.

Internally, it carries an optional raw Bytes.Decode.Decoder (fast path) and a state-passing function (slow path for error reporting).

Decoder context error value
-> Bytes
-> Result (Error context error) value

Run the given decoder on the provided bytes.

First tries the fast path (a single Decode.decode call). If the fast path is unavailable or fails, re-decodes with the slow path to produce a detailed error.

type Error context error
= InContext
{ label : context
, start : Int
}
(Error context error)
| OutOfBounds
{ at : Int
, bytes : Int
}
| Custom { at : Int } error
| BadOneOf { at : Int } (List (Error context error))

Describes errors that arise while decoding.

Custom errors happen through fail, context tracking happens through inContext.

Static

succeed : value -> Decoder context error value

Always succeed with the given value, consuming no input.

fail : error -> Decoder context error value

Always fail with the given error.

This decoder has no fast path — using it (even in a branch of andThen) will cause decode to fall back to the slow path for error reporting.

context
-> Decoder context error value
-> Decoder context error value

Add context to errors that may occur during decoding.

The fast path is preserved (context only matters on the error path).

Primitives

Integers

unsignedInt8 : Decoder context error Int

Decode one byte as an unsigned integer from 0 to 255.

unsignedInt16 : Endianness -> Decoder context error Int

Decode two bytes as an unsigned integer from 0 to 65535.

unsignedInt32 : Endianness -> Decoder context error Int

Decode four bytes as an unsigned integer from 0 to 4294967295.

signedInt8 : Decoder context error Int

Decode one byte as a signed integer from -128 to 127.

signedInt16 : Endianness -> Decoder context error Int

Decode two bytes as a signed integer from -32768 to 32767.

signedInt32 : Endianness -> Decoder context error Int

Decode four bytes as a signed integer from -2147483648 to 2147483647.

Floats

float16 : Endianness -> Decoder context error Float

Decode two bytes as a 16-bit float.

float32 : Endianness -> Decoder context error Float

Decode four bytes as a 32-bit float.

float64 : Endianness -> Decoder context error Float

Decode eight bytes as a 64-bit float.

Strings

string : Int -> Decoder context error String

Decode exactly n bytes as a UTF-8 string.

Raw Bytes

bytes : Int -> Decoder context error Bytes

Decode exactly n bytes.

Mapping

map : (a -> b) -> Decoder context error a -> Decoder context error b

Transform the value a decoder produces.

(x -> y -> z)
-> Decoder context error x
-> Decoder context error y
-> Decoder context error z

Combine what 2 decoders produce.

(w -> x -> y -> z)
-> Decoder context error w
-> Decoder context error x
-> Decoder context error y
-> Decoder context error z

Combine what 3 decoders produce.

(v -> w -> x -> y -> z)
-> Decoder context error v
-> Decoder context error w
-> Decoder context error x
-> Decoder context error y
-> Decoder context error z

Combine what 4 decoders produce.

(u -> v -> w -> x -> y -> z)
-> Decoder context error u
-> Decoder context error v
-> Decoder context error w
-> Decoder context error x
-> Decoder context error y
-> Decoder context error z

Combine what 5 decoders produce.

Pipeline

Decoder context error a
-> Decoder context error (a -> b)
-> Decoder context error b

Decode a value and apply it to the function held by the previous decoder.

import Bytes.Decoder as BD


BD.succeed Record
    |> BD.keep BD.unsignedInt8
    |> BD.keep BD.unsignedInt8
    |> BD.keep BD.unsignedInt8
Decoder context error ignore
-> Decoder context error keep
-> Decoder context error keep

Run a decoder but discard its result, keeping the previous value.

The ignored decoder must still succeed for the pipeline to succeed.

Chaining

(a -> Decoder context error b)
-> Decoder context error a
-> Decoder context error b

Decode one thing, then decode another thing based on the result.

The fast path uses Decode.andThen. If the callback returns a decoder without a fast path (e.g. via fail), the fast decoder signals failure via Decode.fail, causing decode to fall back to the slow path.

oneOf : List (Decoder context error value) -> Decoder context error value

Try each decoder in order, returning the first success.

This decoder has no fast path — it always uses the slow path with backtracking. For tag-based dispatch, prefer andThen which preserves the fast path.

Collects all errors on failure for diagnostic purposes.

Caution: oneOf backtracks by byte offset, not by meaning. If two branches consume the same number of bytes but interpret them differently (e.g. unsignedInt32 vs float32), the first branch that successfully reads those bytes wins — even if the value makes no sense for your domain. In general, raw byte sequences are untagged, so oneOf cannot distinguish between alternatives that happen to have the same byte layout. Prefer andThen with an explicit tag byte whenever possible.

Decoder context error value
-> Int
-> Decoder context error (List value)

Decode exactly count copies of the same decoder into a list.

If the inner decoder has a fast path, repeat preserves it by composing a single Decode.loop decoder.

type Step state a
= Loop state
| Done a

Represent the next step of a loop: either continue with updated state, or finish with a final value.

(state -> Decoder context error (Step state a))
-> state
-> Decoder context error a

Decode in a loop. Each iteration produces either Loop (continue with new state) or Done (finish with a value).

The fast path uses Decode.loop (a tight while loop in the JS kernel). If any iteration's callback returns a decoder without a fast path, the fast decoder signals failure via Decode.fail and decode falls back to the slow path.

Random access

An opaque position in the input, obtained via position.

position : Decoder context error Position

Produce the current byte offset in the input.

This decoder has no fast path — using it will cause decode to fall back to the slow path.

import Bytes.Decoder as BD


BD.decode BD.position (fromList [ 1, 2, 3 ])
--> Ok BD.startOfInput


BD.decode
    (BD.unsignedInt8
        |> BD.andThen (\_ -> BD.position)
    )
    (fromList [ 1, 2, 3 ])
    |> Result.map (\\p -> p == BD.startOfInput)
--> Ok False

The position at the very start of the input (offset 0).

Useful as the relativeTo argument in randomAccess.

{ offset : Int
, relativeTo : Position
}
-> Decoder context error value
-> Decoder context error value

Read data at an arbitrary offset, then resume where you left off.

This decoder has no fast path.

As an example, consider data laid out as:

  • A byte giving the length of a string
  • A byte giving the absolute offset to the string
  • Another byte we also need
import Bytes exposing (Bytes)
import Bytes.Decoder as BD exposing (Decoder)
import Bytes.Encode as E


input : Bytes
input =
    [ E.unsignedInt8 5
    , E.unsignedInt8 15
    , E.unsignedInt8 6
    , E.string (String.repeat 12 "\u{0000}")
    , E.string "hello"
    ]
        |> E.sequence
        |> E.encode


stringAtOffset : Decoder c e String
stringAtOffset =
    BD.map2 Tuple.pair BD.unsignedInt8 BD.unsignedInt8
        |> BD.andThen
            (\( len, offset ) ->
                BD.randomAccess
                    { offset = offset, relativeTo = BD.startOfInput }
                    (BD.string len)
            )


BD.decode stringAtOffset input
--> Ok "hello"

Parsing continues at the sequential position after randomAccess returns:

final : Decoder c e { string : String, number : Int }
final =
    BD.succeed (\s n -> { string = s, number = n })
        |> BD.keep stringAtOffset
        |> BD.keep BD.unsignedInt8


BD.decode final input
--> Ok { string = "hello", number = 6 }

Advanced

fromDecoderUnsafe : error -> Int -> Decoder value -> Decoder context error value

Wrap a raw Bytes.Decode.Decoder into a Decoder with error reporting.

This is an escape hatch for advanced users who already have a working Bytes.Decode.Decoder and want to use it within this library's decoder pipeline. You must supply:

  • error — the error to report if the inner decoder fails
  • byteLength — the exact number of bytes the inner decoder consumes on success
  • the raw Bytes.Decode.Decoder to wrap

The recommended approach is to rewrite your decoder using the functions provided by this module — they cover all the functionality of the original Bytes.Decode module while providing error reporting and oneOf branching.

This function is unsafe because the caller is responsible for providing the correct byte width. If the byte length is wrong, the offset tracking in the slow path will be incorrect, leading to corrupt subsequent decoding. This also means it must not be used for variable-length decoders (e.g. decoders that read a length prefix and then consume that many bytes) — only for fixed-width decoders where the byte count is known statically.