sailfish package - github.com/JekaMas/sailfish - Go Packages

sailfish

package module
v1.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 15, 2026 License: MIT Imports: 7 Imported by: 0

README

sailfish

sailfish is a fast, unsigned, fixed-scale decimal package for trading and financial protocols that exchange exact values as strings such as "123.31232".

The numeric state is one scaled integer:

value = units / 10^venue scale

Supported unit backends are uint8, uint16, uint32, uint64, and uint256.Int. The common parse, append, compare, and arithmetic paths perform no heap allocations.

Sailfish requires Go 1.26.5 or newer.

The current release is v1.0.1. On the documented Apple M1 Max / Go 1.26.5 benchmark host, runtime-scale uint256 parsing is 8.69 ns and direct uint256 CBOR decode is 4.20 ns; both track their measured same-binary implementation kernels and perform no heap allocations. See BENCHMARKS.md and PERFORMANCE.md for the complete matrix and rejected alternatives.

Single-format policy

main contains one current implementation and one canonical wire format. It does not retain compatibility codecs, legacy decoders, alternate encodings, or compatibility fallback implementations. Decimal CBOR is always the preferred shortest unsigned integer representation, using tag 2 only when a uint256.Int does not fit in uint64. Input in any other representation is rejected instead of being normalized or decoded by an older path.

Optimizations replace the previous implementation after benchmarks and the complete correctness suite pass. They do not add parallel numbered codec versions.

Quick start

Select semantic kind, unit capacity, and fractional scale explicitly:

codec, err := sailfish.NewCodec[sailfish.PriceUint64[sailfish.Fraction5]]()
if err != nil {
	return err
}

price, err := codec.Parse("123.31232")
if err != nil {
	return err
}

delta, err := codec.Parse("0.00001")
if err != nil {
	return err
}

if overflow := price.AddAssign(delta); overflow {
	return sailfish.ErrOverflow
}

request := make([]byte, 0, 32)
request = codec.AppendTo(request, price)
// request == "123.31233"

For PriceUint64[Fraction9], the maximum representable value is 18446744073.709551615.

Choosing a type

Choose semantic kind, fractional scale, and integer capacity independently. The format type carries all three choices, so a price cannot be passed where an amount is required even when both use the same scale and backend.

Typical value Suggested format Why
Small bounded ratio or rate PriceUint16[Fraction4] Compact units with four exact fractional digits
CEX price or quantity PriceUint64[Fraction5], AmountUint64[Fraction8] Native arithmetic with explicit venue precision
Token amount AmountUint256[Fraction18] Full EVM-width scaled units
Runtime venue metadata Uint256Codec Scale is validated once without storing it per value

Use the narrowest backend whose complete scaled-integer range covers the protocol contract. Fractional scale alone does not determine the backend.

Construction patterns

Parse canonical venue text with a cached codec on repeated paths:

type PriceFormat = sailfish.PriceUint64[sailfish.Fraction5]
type Price = sailfish.Decimal[PriceFormat, uint64]

priceCodec, err := sailfish.NewCodec[PriceFormat]()
if err != nil {
	return err
}
price, err := priceCodec.Parse("123.31232")
if err != nil {
	return err
}

Construct directly from already-scaled protocol units without a text round-trip:

type AmountFormat = sailfish.AmountUint32[sailfish.Fraction6]

amount, err := sailfish.NewFromUnits[AmountFormat](uint32(1_234_567))
if err != nil {
	return err
}
// amount.String() == "1.234567"

Use distinct formats for domain boundaries:

type CEXPrice = sailfish.Decimal[
	sailfish.PriceUint64[sailfish.Fraction5],
	uint64,
]
type TokenAmount = sailfish.Decimal[
	sailfish.AmountUint256[sailfish.Fraction18],
	uint256.Int,
]

For a scale supplied by trusted venue metadata, validate it once and parse into caller-owned storage:

codec, err := sailfish.NewUint256Codec(18)
if err != nil {
	return err
}
var units uint256.Int
if err := codec.ParseInto("1.250000000000000000", &units); err != "" {
	return err
}

Scale and storage range

Fractional scale and integer capacity are independent. A scale-1 price can be 25.5 or 1844674407370955161.5; scale alone cannot select a safe backend. Choose an explicit backend when a narrower range is part of the contract:

type SmallPriceFormat = sailfish.PriceUint16[sailfish.Fraction2]
type SmallPrice = sailfish.Decimal[SmallPriceFormat, uint16]

codec, err := sailfish.NewCodec[SmallPriceFormat]()
if err != nil {
	return err
}
price, err := codec.Parse("655.35")
// codec.MaxIntegerDigits() == 3

The generic format families are:

PriceUint8/16/32/64/256[FractionN]
AmountUint8/16/32/64/256[FractionN]

Price and amount formats remain different types even when backend and scale match. Fraction0 through Fraction20 are provided; custom zero-sized scale types can represent other supported scales.

Backend Maximum units Maximum scale Maximum decimal digits
uint8 255 2 3
uint16 65535 4 5
uint32 4294967295 9 10
uint64 18446744073709551615 19 20
uint256.Int 2^256 - 1 77 78

Codec.MaxIntegerDigits reports the maximum integer-part digit count for a format. It is a capacity description, not a promise that every number with that many digits fits the binary backend.

There is one format API: PriceUint* and AmountUint*. Cached Codec operations resolve scale once and benchmark equivalently to a test-local concrete venue; use a codec on hot paths. Each generic format embeds a concrete backend, so it does not pay generic backend dispatch.

Wide values

The common 18-decimal on-chain amount is explicit:

type AmountFormat = sailfish.AmountUint256[sailfish.Fraction18]
type Amount = sailfish.Decimal[AmountFormat, uint256.Int]

amountCodec, err := sailfish.NewCodec[AmountFormat]()
if err != nil {
	return err
}

The format selects semantic kind, fractional scale, and unit backend. The sealed unit-provider interface prevents pairing a format with the wrong unit type.

When trusted venue metadata resolves a scale at runtime, use the concrete Uint256Codec to avoid generic venue dispatch:

codec, err := sailfish.NewUint256Codec(6)
if err != nil {
	return err
}

var units uint256.Int
if err := codec.ParseInto("123.456789", &units); err != "" {
	return err
}

dst := codec.AppendTo(make([]byte, 0, 32), units)

Uint256Codec stores the validated scale once. It does not attach a dynamic scale to each value; callers remain responsible for selecting the codec from canonical venue metadata.

Parsing and ownership

Parsing is strict. Constructors do not trim input and do not accept signs, exponents, missing integer/fraction digits, or excess precision.

codec.Parse(s)        // retains s only when it is already canonical
codec.ParseCompact(s) // never retains s
codec.ParseBytes(b)   // parses bytes directly and never retains them

Non-canonical accepted input is normalized only while constructing the value:

"001.2" at scale 5 -> "1.20000"

Use ParseCompact when a short input string may reference a much larger response buffer.

Immutable string representation

Decimal may retain canonical wire text:

representation string

A string header is smaller than a byte-slice header and String can return it without conversion or allocation. A Go string cannot be safely extended or edited in place. Arithmetic therefore updates units and invalidates only the header:

d.units = newUnits
d.representation = ""

This invalidation allocates nothing. Any string returned before the mutation remains immutable and valid. After mutation:

  • AppendTo remains allocation-free when its destination has capacity.
  • String creates one newly owned string.
  • Canonical returns a copy that retains that string once.

There is no mutable lazy cache, so concurrent reads do not race.

Core API

Need API
Parse retained canonical text New, Codec.Parse
Parse without retaining input NewCompact, NewBytes, codec equivalents
Construct/read scaled units NewFromUnits, Codec.FromUnits, Units
Validate and cache a static format NewCodec, Codec.Scale, Codec.MaxIntegerDigits
Replace or inspect value state SetUnits, IsZero, HasRepresentation
Exact encoded lengths Len, CBORLen, codec equivalents
Runtime-scale uint256 text NewUint256Codec, Parse, ParseBytes, ParseInto, ParseBytesInto, AppendTo
Caller-buffer serialization AppendTo, AppendJSON, AppendText
Caller-buffer CBOR AppendCBOR, Codec.AppendCBOR, Uint256Codec.AppendCBOR
Strict CBOR decode UnmarshalCBOR, Codec.ParseCBOR, Uint256Codec.ParseCBOR, Uint256Codec.ParseCBORInto
Positional-array CBOR decode Codec.ParseCBORFirst, Uint256Codec.ParseCBORFirst, Uint256Codec.ParseCBORFirstInto
Owned or retained serialization String, Canonical, MarshalText, MarshalJSON, MarshalCBOR
Same-venue ordering Compare, Cmp, Equal, Less methods
Cross-scale/backend ordering package-level Compare
Checked arithmetic Add, Sub, AddAssign, SubAssign
Overflow-style arithmetic AddOverflow, SubUnderflow

Use Codec[V, U] for repeated work with a compile-time format. Its zero value is valid; NewCodec additionally validates and caches the format metadata. Use Uint256Codec when a trusted venue definition supplies scale at runtime. Its Into methods leave the destination unchanged on error. Invalid formats, inputs, and arithmetic return errors or status values; the package does not use panics as an API contract.

Serialization and deserialization

Sailfish exposes owned standard interfaces and caller-buffer APIs. Use the owned forms at ordinary application boundaries and append/prefix-decode forms for MDBX records, network frames, and other hot aggregate codecs.

Format Encode Decode Wire contract
Canonical text AppendText, MarshalText UnmarshalText Exact fixed-scale ASCII decimal
JSON AppendJSON, MarshalJSON UnmarshalJSON Quoted decimal string; bare numbers rejected
CBOR scalar AppendCBOR, MarshalCBOR ParseCBOR, UnmarshalCBOR Preferred unsigned integer or tag-2 bignum
Positional CBOR repeated AppendCBOR repeated ParseCBORFirst Decimal scalars inside a parent toarray record
Text and JSON

JSON values are quoted decimal strings. Bare JSON numbers are rejected. JSON integration and escaped-string decoding use github.com/goccy/go-json; ordinary unescaped decimal strings decode directly from the JSON input.

text, err := price.MarshalText() // []byte("123.31232")
if err != nil {
	return err
}
jsonValue, err := price.MarshalJSON() // []byte("\"123.31232\"")
if err != nil {
	return err
}

var decoded Price
if err := decoded.UnmarshalJSON(jsonValue); err != nil {
	return err
}

AppendText and AppendJSON reuse caller capacity. MarshalText and MarshalJSON return owned slices and therefore allocate their result once. The direct JSON decoder parses ordinary quoted decimals from the input bytes without allocating. Escaped JSON strings take the standards-compliant go-json unescape path before decimal parsing.

For a hot aggregate encoder, reuse caller capacity rather than asking each field for an owned result:

wire := make([]byte, 0, 32)
wire = price.AppendJSON(wire[:0])

MarshalJSON reserves the exact native/retained size. For an unretained uint256.Int, it reserves the bounded maximum and performs the expensive wide decimal split only once.

Compact deterministic CBOR

CBOR stores only the scaled unsigned integer. The decimal scale and semantic kind are compile-time format identity, while retained source text is cache state; none of them is duplicated in storage.

Decimal[PriceUint64[Fraction5]] units 12331232 -> 1a00bc28e0
Decimal[AmountUint256[Fraction18]] units 2^64 -> c249010000000000000000

Native values use RFC 8949's shortest unsigned-integer representation. A uint256.Int uses the same representation while it fits in uint64, then tag 2 with a minimal big-endian magnitude. Decode accepts only preferred, definite-length encodings and rejects trailing data, longer integer forms, leading-zero bignums, and values outside the selected unit backend.

Sailfish decimals implement MarshalCBOR and UnmarshalCBOR for github.com/fxamacker/cbor/v2. They remain scalar elements inside a compact parent array:

type Quote struct {
	_ struct{} `cbor:",toarray"`

	Price  sailfish.Decimal[PriceFormat, uint64]
	Amount sailfish.Decimal[AmountFormat, uint256.Int]
}

This encodes as [priceUnits, amountUnits], not nested one-element arrays. Wire sizes are 1-9 bytes for native units and 1-35 bytes for uint256 units, before the enclosing array header.

Cache fxamacker modes for reflective or cold-path aggregate encoding:

enc, err := cbor.CanonicalEncOptions().EncMode()
if err != nil {
	return err
}
dec, err := cbor.DecOptions{}.DecMode()
if err != nil {
	return err
}

raw, err := enc.Marshal(quote)
if err != nil {
	return err
}
var decoded Quote
if err := dec.Unmarshal(raw, &decoded); err != nil {
	return err
}

Use AppendCBOR or the cached codec equivalent when building a hot MDBX value:

dst := make([]byte, 0, 1+2*sailfish.MaxCBORSize)
dst = append(dst, 0x82) // fixed two-field CBOR array
dst = priceCodec.AppendCBOR(dst, price)
dst = amountCodec.AppendCBOR(dst, amount)

Decode decimal fields from a manual positional array without first finding or copying each scalar item:

price, raw, err := priceCodec.ParseCBORFirst(raw)
if err != nil {
	return err
}
amount, raw, err := amountCodec.ParseCBORFirst(raw)
if err != nil {
	return err
}

ParseCBORFirst consumes exactly one preferred deterministic unsigned value and returns the unconsumed suffix. ParseCBOR remains the whole-item API and rejects trailing data. On failure, prefix decoders return no suffix and ParseCBORFirstInto leaves its destination unchanged.

The hot positional path must validate the parent array header and field count at the enclosing-record layer. Sailfish then validates each scalar's preferred encoding, backend range, and complete consumption. There is one current CBOR format: no compatibility decoder, alternate integer form, or legacy fallback.

These append APIs and all direct decode APIs are 0 B/op, 0 allocs/op with a sized caller buffer. MarshalCBOR necessarily allocates one owned result slice. The reflective fxamacker parent marshal also invokes that owned-slice interface for each decimal; use the append path when aggregate encoding must remain allocation-free. Cache fxamacker EncMode and DecMode for generic or cold paths; they are configured codec instances, not interfaces implemented by application values. Reflective toarray decode remains allocation-free for the tested fixed quote shape.

A permanent fourteen-field oracle test builds a 93-byte positional record with cached Sailfish codecs and verifies byte-for-byte equality with deterministic fxamacker cbor:",toarray". The 93-byte result belongs to that synthetic value set; it is neither a fixed record size nor a theoretical minimum. The same schema has a 15-byte structural floor when its symbol is empty and every numeric value is zero. It has no finite format-wide maximum until the enclosing record bounds symbol length.

A separate July 15, 2026 snapshot covers 100 MEXC spot, 100 Hyperliquid spot, and 100 Hyperliquid perpetual markets, ranked by reported 24-hour volume. It uses distinct positive price and quantity observations from venue metadata, context, ticker, and L2 book responses. Each market identity appears once in the fixture; observed values are deduplicated before min/quantile/max selection. For realistic nonzero records, the resulting 14-field wires are 48-78 bytes:

Cohort Quantity case Min p50 p95 Max Mean
MEXC spot min / median / max 55 / 59 / 62 60 / 63 / 64 68 / 71 / 74 75 / 78 / 78 60.96 / 63.66 / 65.92
Hyperliquid spot min / median / max 48 / 48 / 48 56 / 60 / 60 64 / 66 / 68 69 / 71 / 73 57.42 / 60.18 / 60.80
Hyperliquid perps min / median / max 53 / 56 / 57 56 / 60 / 60 64 / 67 / 68 66 / 68 / 69 57.86 / 60.61 / 61.52

The snapshot and its invariant checks live in testdata/market_cbor_samples.json and market_cbor_benchmark_test.go. Its direct encode path is allocation-free; decode allocates only when the parent record must own a symbol string. Sailfish numeric field decode remains allocation-free. See BENCHMARKS.md for source policy and repeated timing results.

Errors

Errors are typed string constants:

type Error string

const ErrSyntax Error = "sailfish: invalid syntax"

They are comparable, allocation-free to return, and work with errors.Is. Sailfish does not expose panic-on-error constructors. Invalid scale and input configuration are returned as errors. A zero Codec[V, U] derives the valid compile-time venue scale; a zero Uint256Codec is the useful scale-0 codec.

Range model

The complete digit sequence is one scaled integer, so scale consumes integer range.

Backend Maximum scale Raw units
uint8 2 1 byte
uint16 4 2 bytes
uint32 9 4 bytes
uint64 19 8 bytes
uint256.Int 77 32 bytes

On 64-bit systems:

Decimal[..., uint8]        24 bytes
Decimal[..., uint16]       24 bytes
Decimal[..., uint32]       24 bytes
Decimal[..., uint64]       24 bytes
Decimal[..., uint256.Int]  48 bytes
Codec                       1 byte

Narrow native units enforce smaller ranges and reduce standalone/raw unit arrays. They do not reduce the current Decimal struct below 24 bytes because its retained immutable string header and alignment dominate the layout. The incomparability marker is the first zero-sized field so it does not create trailing zero-field padding. Layout tests lock unit and string offsets, struct alignment, and these sizes on 64-bit targets.

Deliberate boundaries

The initial package does not define:

  • signed decimals;
  • implicit truncation or rounding;
  • multiplication or division rounding policy;
  • floating-point conversion;
  • mutable shared caches;
  • a runtime-varying scale carried by every value.

Those are separate financial contracts, not parser conveniences. Custom zero-sized VenueScale types cover compile-time scales beyond Fraction20.

Performance

These are five-run summaries from a complete make bench execution on Go 1.26.5, darwin/arm64, Apple M1 Max. Microbenchmark numbers are local, not portable guarantees; compare changes on the same host and toolchain.

Parsing and formatting
Operation Time B/op allocs/op
Parse canonical uint64 through Codec 7.75 ns 0 0
Parse canonical uint256.Int 49.2 ns 0 0
Parse maximum 78-digit uint256.Int 64.6 ns 0 0
Append retained uint64 2.90 ns 0 0
Append formatted uint64 12.8 ns 0 0
Append formatted four-limb uint256.Int 112 ns 0 0
Return retained String 2.12 ns 0 0
Return newly formatted String 27.0 ns 16 1
Width scaling
Dense parse kernel 19 digits 38 digits 57 digits 77 digits
string input 9.56 ns 18.9 ns 28.5 ns 43.3 ns
[]byte input 9.39 ns 18.2 ns 28.0 ns 42.8 ns
Formatted width One limb Two limbs Three limbs Four limbs Maximum
Wide formatting kernel 17.0 ns 43.6 ns 69.2 ns 107 ns 129 ns

Every width-scaling parse and append row above is 0 B/op, 0 allocs/op.

Comparison and arithmetic
Operation Time B/op allocs/op
Same-scale uint64 compare 2.10 ns 0 0
Same-scale uint256.Int compare 6.37 ns 0 0
Cross-scale/backend compare 50.7 ns 0 0
Checked uint64 add-assign 4.38 ns 0 0
Checked uint256.Int add-assign 13.2 ns 0 0
Serialization
Operation Time B/op allocs/op
Append retained / formatted native JSON 4.43 / 15.6 ns 0 0
Append retained / formatted wide JSON 7.46 / 139 ns 0 0
Owned native retained / formatted MarshalJSON 21.1 / 38.3 ns 16 1
Owned wide retained / formatted MarshalJSON 36.2 / 175 ns 96 1
Unmarshal canonical native / wide JSON 14.4 / 78.8 ns 0 0
Unmarshal escaped native JSON 120 ns 40 2
Append native / uint256 CBOR scalar 3.54 / 8.03 ns 0 0
Decode native / uint256 CBOR scalar 8.07 / 12.8 ns 0 0
Runtime-codec uint256 append, one limb / maximum 4.03 / 6.52 ns 0 0
Runtime-codec uint256 decode, one limb / maximum 4.17 / 5.81 ns 0 0
Owned native / uint256 MarshalCBOR 20.2 / 28.3 ns 16 / 32 1
fxamacker two-field toarray marshal / unmarshal 175 / 146 ns 120 / 0 4 / 0
Manual 14-field positional CBOR encode / decode 50.1 / 93.8 ns 0 / 8 0 / 1

The manual record decoder's one allocation owns its parent string field; Sailfish numeric field decoding is allocation-free. Owned String and marshal results allocate by contract. Detailed commands, profiles, and allocation ownership are in BENCHMARKS.md.

For values parsed from canonical venue text, retain the representation with Codec.Parse; subsequent appends are below 5 ns even for a four-limb value. Use raw-unit formatting for constructed or mutated values, and call Canonical once when the same formatted value will be emitted repeatedly.

The amd64/arm64 SWAR loader uses one narrowly scoped read-only unsafe load; other architectures use the byte-shift loader. The pointer is never retained or used for mutation, and release validation includes cross-builds, race, and checkptr=2. No assembly or runtime CPU-feature dispatch is used: measured short-token latency does not justify their call and maintenance cost.

Algorithms and measured choices

Area Current algorithm Reason
Scale model Zero-sized compile-time format or one-byte Uint256Codec Static strategies carry no runtime scale; dynamic venue metadata validates scale once
Numeric model One unsigned scaled integer Exact comparison/arithmetic with no floating-point state
Native parsing Pairwise accumulation plus known-point SWAR for exact 8/16-digit shapes Keeps irregular inputs simple while bringing the common 123.31232 parse to 7.75 ns
Wide parsing One or two independent eight-digit SWAR blocks plus a scalar tail Reduced 19-78 digit parse time by roughly 9-19% in the latest round
SWAR loads One read-only unaligned native load on amd64/arm64; byte shifts elsewhere Removes load assembly on release architectures without retaining or mutating input memory
Native formatting Pairwise digits plus direct integer/fraction placement Avoids a temporary decimal-point prefix copy
Wide formatting Base-1e19 chunks using precomputed-reciprocal 2-by-1 division Avoids serial hardware division and reduced two-to-four-limb formatting by roughly 9-24%
Repeated output Retain immutable canonical input or call Canonical once Repeated append becomes a short string copy
JSON Direct quoted append and parse-first unescaped decode Keeps canonical JSON encode/decode allocation-free; escaped input uses the standards-compliant slow path
CBOR Preferred unsigned integer; tag 2 only above uint64 Small deterministic wire with strict decoding
Hot aggregate CBOR Caller-buffer scalar append and positional prefix decode Avoids reflection and owned per-field slices
Type dispatch Concrete backend embedded in each format; cached scale in Codec Avoids generic backend type switches and repeated metadata work
Errors Pre-boxed typed string constants Comparable errors with zero per-call failure allocation

Measured alternatives are not retained in production: base-1e9 wide formatting was 14-56% slower, direct decimal placement across wide chunks was 2-5% slower, base-1e8 native formatting regressed every measured width, generated per-scale masks duplicated code for a narrower kernel, and a 0-99 cache penalized representative misses. See PERFORMANCE.md for the benchmark artifacts and acceptance decisions.

Validation

make test
make vet
make race
make bench
make fuzz

Tests include exhaustive byte validation, maximum-value boundaries, randomized exact-reference properties, ownership/cache behavior, allocation assertions, external-package API checks, and fuzz targets for both unit backends and JSON.

If this repository is cloned under a parent directory containing an unrelated go.work, use GOWORK=off or the included Makefile.

License

MIT. See LICENSE.

Documentation

Overview

Package sailfish provides fast, unsigned, fixed-scale decimal values for trading and financial protocols.

A value is stored as one scaled integer:

value = units / 10^scale

The package supports uint8, uint16, uint32, uint64, and uint256.Int units. Fractional scale and backend capacity are selected independently. Its hot parse, text/CBOR append, strict CBOR decode, compare, and arithmetic paths are allocation-free when caller-owned output buffers have enough capacity.

Index

Examples

Constants

View Source
const MaxCBORSize = 35

MaxCBORSize is the maximum preferred CBOR encoding size of one Decimal. It is tag 2, a one-byte length argument, and a 32-byte uint256 magnitude.

Variables

This section is empty.

Functions

func Compare

func Compare[VA Venue[UA], UA Unit, VB Venue[UB], UB Unit](
	a Decimal[VA, UA],
	b Decimal[VB, UB],
) int

Compare compares decimals across scales and unit backends exactly. It does not rescale either integer, so comparison cannot overflow.

Types

type AmountUint8

type AmountUint8[S VenueScale] struct {
	Uint8Units
}

AmountUint8 through AmountUint256 are the amount-kind equivalents. Price and amount formats remain distinct types even with equal scale and backend.

func (AmountUint8[S]) NotionScale

func (AmountUint8[S]) NotionScale() Notion

type AmountUint16

type AmountUint16[S VenueScale] struct {
	Uint16Units
}

func (AmountUint16[S]) NotionScale

func (AmountUint16[S]) NotionScale() Notion

type AmountUint32

type AmountUint32[S VenueScale] struct {
	Uint32Units
}

func (AmountUint32[S]) NotionScale

func (AmountUint32[S]) NotionScale() Notion

type AmountUint64

type AmountUint64[S VenueScale] struct {
	Uint64Units
}

func (AmountUint64[S]) NotionScale

func (AmountUint64[S]) NotionScale() Notion

type AmountUint256

type AmountUint256[S VenueScale] struct {
	Uint256Units
}

func (AmountUint256[S]) NotionScale

func (AmountUint256[S]) NotionScale() Notion

type Codec

type Codec[V Venue[U], U Unit] struct {
	// contains filtered or unexported fields
}

Codec validates a venue once and carries its scale through repeated parse/format operations. It is the preferred hot-loop API. Its zero value is usable and derives scale from the compile-time venue; NewCodec validates and caches that scale for the hot path.

The one-byte scalePlusOne encoding reserves zero for zero-value derivation.

Example (ManualPositionalCBOR)
package main

import (
	"fmt"

	"github.com/JekaMas/sailfish"
	"github.com/holiman/uint256"
)

type examplePriceFormat = sailfish.PriceUint64[sailfish.Fraction5]

type exampleAmountFormat = sailfish.AmountUint256[sailfish.Fraction18]

func main() {
	priceCodec, err := sailfish.NewCodec[examplePriceFormat]()
	if err != nil {
		fmt.Println(err)
		return
	}
	amountCodec, err := sailfish.NewCodec[exampleAmountFormat]()
	if err != nil {
		fmt.Println(err)
		return
	}
	price := priceCodec.FromUnits(12_331_232)
	var amountUnits uint256.Int
	amountUnits.SetUint64(1_250_000_000_000_000_000)
	amount := amountCodec.FromUnits(amountUnits)

	record := make([]byte, 0, 1+2*sailfish.MaxCBORSize)
	record = append(record, 0x82) // fixed two-field CBOR array
	record = priceCodec.AppendCBOR(record, price)
	record = amountCodec.AppendCBOR(record, amount)

	raw := record[1:]
	decodedPrice, raw, err := priceCodec.ParseCBORFirst(raw)
	if err != nil {
		fmt.Println(err)
		return
	}
	decodedAmount, raw, err := amountCodec.ParseCBORFirst(raw)
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println(decodedPrice.String())
	fmt.Println(decodedAmount.String())
	fmt.Println(len(raw))
}
Output:
123.31232
1.250000000000000000
0
Example (Price)
package main

import (
	"fmt"

	"github.com/JekaMas/sailfish"
)

type examplePriceFormat = sailfish.PriceUint64[sailfish.Fraction5]

func main() {
	codec, err := sailfish.NewCodec[examplePriceFormat]()
	if err != nil {
		fmt.Println(err)
		return
	}

	price, err := codec.Parse("123.31232")
	if err != nil {
		fmt.Println(err)
		return
	}
	next, err := price.Add(codec.FromUnits(1))
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println(next.String())
	fmt.Println(next.Units())
}
Output:
123.31233
12331233

func NewCodec

func NewCodec[V Venue[U], U Unit]() (Codec[V, U], error)

func (Codec[V, U]) AppendCBOR

func (c Codec[V, U]) AppendCBOR(dst []byte, d Decimal[V, U]) []byte

AppendCBOR appends preferred deterministic CBOR after validating the codec.

func (Codec[V, U]) AppendJSON

func (c Codec[V, U]) AppendJSON(dst []byte, d Decimal[V, U]) []byte

func (Codec[V, U]) AppendTo

func (c Codec[V, U]) AppendTo(dst []byte, d Decimal[V, U]) []byte

func (Codec[V, U]) CBORLen

func (c Codec[V, U]) CBORLen(d Decimal[V, U]) int

CBORLen returns the exact preferred CBOR size after validating the codec.

func (Codec[V, U]) Canonical

func (c Codec[V, U]) Canonical(d Decimal[V, U]) Decimal[V, U]

func (Codec[V, U]) FromUnits

func (c Codec[V, U]) FromUnits(units U) Decimal[V, U]

func (Codec[V, U]) Len

func (c Codec[V, U]) Len(d Decimal[V, U]) int

func (Codec[V, U]) MaxIntegerDigits

func (c Codec[V, U]) MaxIntegerDigits() int

MaxIntegerDigits reports how many decimal digits can occur before the point in this backend's maximum value at the configured scale. It describes capacity independently from fractional scale; it does not imply that every value with that many digits fits the binary backend.

func (Codec[V, U]) Parse

func (c Codec[V, U]) Parse(s string) (Decimal[V, U], error)

Parse retains s only when it is already canonical fixed-scale text.

func (Codec[V, U]) ParseBytes

func (c Codec[V, U]) ParseBytes(b []byte) (Decimal[V, U], error)

ParseBytes parses b directly and never retains it.

func (Codec[V, U]) ParseCBOR

func (c Codec[V, U]) ParseCBOR(raw []byte) (Decimal[V, U], error)

ParseCBOR decodes preferred deterministic CBOR without retaining raw input.

func (Codec[V, U]) ParseCBORFirst

func (c Codec[V, U]) ParseCBORFirst(raw []byte) (Decimal[V, U], []byte, error)

ParseCBORFirst decodes one preferred deterministic CBOR decimal from the start of raw and returns the unconsumed suffix. It is the typed hot-path decoder for decimal fields inside manually encoded positional arrays. ParseCBOR remains the strict whole-item API.

func (Codec[V, U]) ParseCompact

func (c Codec[V, U]) ParseCompact(s string) (Decimal[V, U], error)

ParseCompact never retains s.

func (Codec[V, U]) Scale

func (c Codec[V, U]) Scale() Notion

func (Codec[V, U]) String

func (c Codec[V, U]) String(d Decimal[V, U]) string

type Decimal

type Decimal[V Venue[U], U Unit] struct {
	// contains filtered or unexported fields
}

Decimal is an unsigned fixed-scale decimal stored as one scaled integer.

Numeric value = units / 10^venue-scale.

representation is optional immutable wire text. Numeric mutation clears the string header; it never edits string bytes. Clearing the header allocates nothing, and strings previously returned by String remain valid.

Example (Serialization)
package main

import (
	"fmt"

	"github.com/JekaMas/sailfish"
	"github.com/fxamacker/cbor/v2"

	json "github.com/goccy/go-json"
	"github.com/holiman/uint256"
)

type examplePriceFormat = sailfish.PriceUint64[sailfish.Fraction5]
type examplePrice = sailfish.Decimal[examplePriceFormat, uint64]
type exampleAmountFormat = sailfish.AmountUint256[sailfish.Fraction18]
type exampleAmount = sailfish.Decimal[exampleAmountFormat, uint256.Int]

type exampleQuote struct {
	_ struct{} `cbor:",toarray"`

	Price  examplePrice
	Amount exampleAmount
}

func main() {
	priceCodec, err := sailfish.NewCodec[examplePriceFormat]()
	if err != nil {
		fmt.Println(err)
		return
	}
	amountCodec, err := sailfish.NewCodec[exampleAmountFormat]()
	if err != nil {
		fmt.Println(err)
		return
	}
	price, err := priceCodec.Parse("123.31232")
	if err != nil {
		fmt.Println(err)
		return
	}
	amount, err := amountCodec.Parse("1.250000000000000000")
	if err != nil {
		fmt.Println(err)
		return
	}
	quote := exampleQuote{Price: price, Amount: amount}

	jsonRaw, err := json.Marshal(quote.Price)
	if err != nil {
		fmt.Println(err)
		return
	}
	enc, err := cbor.CanonicalEncOptions().EncMode()
	if err != nil {
		fmt.Println(err)
		return
	}
	cborRaw, err := enc.Marshal(quote)
	if err != nil {
		fmt.Println(err)
		return
	}
	dec, err := cbor.DecOptions{}.DecMode()
	if err != nil {
		fmt.Println(err)
		return
	}
	var decoded exampleQuote
	if err := dec.Unmarshal(cborRaw, &decoded); err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println(string(jsonRaw))
	fmt.Println(decoded.Price.String())
	fmt.Println(decoded.Amount.String())
}
Output:
"123.31232"
123.31232
1.250000000000000000

func New

func New[V Venue[U], U Unit](s string) (Decimal[V, U], error)

New parses s. It retains s only when s is already canonical fixed-scale text. Parsing is strict: no whitespace, signs, exponent notation, or excess fractional digits are accepted.

func NewBytes

func NewBytes[V Venue[U], U Unit](b []byte) (Decimal[V, U], error)

NewBytes parses b without retaining or converting it.

func NewCompact

func NewCompact[V Venue[U], U Unit](s string) (Decimal[V, U], error)

NewCompact parses s without retaining its backing storage.

func NewFromUnits

func NewFromUnits[V Venue[U], U Unit](units U) (Decimal[V, U], error)

NewFromUnits constructs a decimal from already-scaled units.

Example
package main

import (
	"fmt"

	"github.com/JekaMas/sailfish"
)

func main() {
	type AmountFormat = sailfish.AmountUint32[sailfish.Fraction6]

	amount, err := sailfish.NewFromUnits[AmountFormat](uint32(1_234_567))
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println(amount.String())
}
Output:
1.234567

func (Decimal[V, U]) Add

func (d Decimal[V, U]) Add(other Decimal[V, U]) (Decimal[V, U], error)

func (*Decimal[V, U]) AddAssign

func (d *Decimal[V, U]) AddAssign(other Decimal[V, U]) (overflow bool)

AddAssign leaves d unchanged on overflow. A value-changing success clears cached text without allocation; adding zero preserves it.

func (Decimal[V, U]) AddOverflow

func (d Decimal[V, U]) AddOverflow(other Decimal[V, U]) (Decimal[V, U], bool)

AddOverflow returns the wrapped sum and reports unit overflow.

func (Decimal[V, U]) AppendCBOR

func (d Decimal[V, U]) AppendCBOR(dst []byte) []byte

AppendCBOR appends the preferred deterministic CBOR encoding. It allocates only when dst has insufficient capacity. When Decimal is a field in a cbor:",toarray" struct, the result is a scalar array element rather than a redundant nested one-element array.

func (Decimal[V, U]) AppendJSON

func (d Decimal[V, U]) AppendJSON(dst []byte) []byte

AppendJSON appends a quoted JSON decimal string. Decimal text contains only ASCII digits and a decimal point, so no escaping pass is needed.

func (Decimal[V, U]) AppendText

func (d Decimal[V, U]) AppendText(dst []byte) ([]byte, error)

AppendText implements the append-style text encoding contract available in current Go versions without requiring a newly owned result slice.

func (Decimal[V, U]) AppendTo

func (d Decimal[V, U]) AppendTo(dst []byte) []byte

AppendTo appends canonical fixed-scale text. It allocates only when dst has insufficient capacity.

func (Decimal[V, U]) CBORLen

func (d Decimal[V, U]) CBORLen() int

CBORLen returns the exact size of the preferred CBOR encoding. Decimal is encoded as its scaled unsigned integer. Scale and retained source text are type/cache metadata and are intentionally absent from the wire format.

func (Decimal[V, U]) Canonical

func (d Decimal[V, U]) Canonical() Decimal[V, U]

Canonical returns a copy retaining canonical text. It never mutates shared state and is safe to use concurrently with readers of the original value.

func (Decimal[V, U]) Cmp

func (d Decimal[V, U]) Cmp(other Decimal[V, U]) int

func (Decimal[V, U]) Compare

func (d Decimal[V, U]) Compare(other Decimal[V, U]) int

Compare returns -1, 0, or +1.

func (Decimal[V, U]) Equal

func (d Decimal[V, U]) Equal(other Decimal[V, U]) bool

func (Decimal[V, U]) HasRepresentation

func (d Decimal[V, U]) HasRepresentation() bool

HasRepresentation reports whether canonical wire text is currently retained.

func (Decimal[V, U]) IsZero

func (d Decimal[V, U]) IsZero() bool

func (Decimal[V, U]) Len

func (d Decimal[V, U]) Len() int

Len returns the exact canonical text length.

func (Decimal[V, U]) Less

func (d Decimal[V, U]) Less(other Decimal[V, U]) bool

func (Decimal[V, U]) MarshalCBOR

func (d Decimal[V, U]) MarshalCBOR() ([]byte, error)

MarshalCBOR implements the fxamacker/cbor Marshaler contract. The returned owned slice necessarily allocates once; use AppendCBOR on hot paths.

func (Decimal[V, U]) MarshalJSON

func (d Decimal[V, U]) MarshalJSON() ([]byte, error)

func (Decimal[V, U]) MarshalText

func (d Decimal[V, U]) MarshalText() ([]byte, error)

func (*Decimal[V, U]) SetUnits

func (d *Decimal[V, U]) SetUnits(units U)

SetUnits replaces the scaled integer. A value-changing update invalidates cached text without allocation; setting the same value preserves it.

func (Decimal[V, U]) String

func (d Decimal[V, U]) String() string

String returns retained text when available. Otherwise it creates exactly one result string allocation and does not mutate d.

func (Decimal[V, U]) Sub

func (d Decimal[V, U]) Sub(other Decimal[V, U]) (Decimal[V, U], error)

func (*Decimal[V, U]) SubAssign

func (d *Decimal[V, U]) SubAssign(other Decimal[V, U]) (underflow bool)

SubAssign leaves d unchanged on underflow. A value-changing success clears cached text without allocation; subtracting zero preserves it.

func (Decimal[V, U]) SubUnderflow

func (d Decimal[V, U]) SubUnderflow(other Decimal[V, U]) (Decimal[V, U], bool)

SubUnderflow returns the wrapped difference and reports unit underflow.

func (Decimal[V, U]) Units

func (d Decimal[V, U]) Units() U

Units returns the scaled integer by value. uint256.Int is an inline four-limb value, so the returned value owns its storage without allocation.

func (*Decimal[V, U]) UnmarshalCBOR

func (d *Decimal[V, U]) UnmarshalCBOR(raw []byte) error

UnmarshalCBOR implements the fxamacker/cbor Unmarshaler contract. It accepts only RFC 8949 preferred deterministic unsigned encodings and leaves d unchanged on failure. Successful decode clears retained text because CBOR carries numeric units only.

func (*Decimal[V, U]) UnmarshalJSON

func (d *Decimal[V, U]) UnmarshalJSON(data []byte) error

UnmarshalJSON parses ordinary quoted decimals directly without a separate escape scan. go-json handles escaped strings and non-string JSON syntax.

func (*Decimal[V, U]) UnmarshalText

func (d *Decimal[V, U]) UnmarshalText(text []byte) error

type Error

type Error string

Error is an allocation-free, comparable package error.

Exported errors are typed string constants. They work with errors.Is when returned directly or wrapped with fmt.Errorf and %w.

const (
	ErrSyntax               Error = "sailfish: invalid syntax"
	ErrRange                Error = "sailfish: value does not fit unit type"
	ErrPrecision            Error = "sailfish: too many fractional digits"
	ErrScale                Error = "sailfish: scale is unsupported by unit type"
	ErrOverflow             Error = "sailfish: addition overflow"
	ErrUnderflow            Error = "sailfish: subtraction underflow"
	ErrNilDestination       Error = "sailfish: nil destination"
	ErrCBORSyntax           Error = "sailfish: invalid CBOR"
	ErrCBORNonDeterministic Error = "sailfish: non-deterministic CBOR"
)

func (Error) Error

func (e Error) Error() string

type Fraction0

type Fraction0 struct{}

Fraction0 through Fraction20 are zero-sized fractional-scale policies. Scale is independent from the scaled-integer backend: callers choose both the number of digits after the point and the required numeric capacity.

func (Fraction0) NotionScale

func (Fraction0) NotionScale() Notion

type Fraction1

type Fraction1 struct{}

func (Fraction1) NotionScale

func (Fraction1) NotionScale() Notion

type Fraction2

type Fraction2 struct{}

func (Fraction2) NotionScale

func (Fraction2) NotionScale() Notion

type Fraction3

type Fraction3 struct{}

func (Fraction3) NotionScale

func (Fraction3) NotionScale() Notion

type Fraction4

type Fraction4 struct{}

func (Fraction4) NotionScale

func (Fraction4) NotionScale() Notion

type Fraction5

type Fraction5 struct{}

func (Fraction5) NotionScale

func (Fraction5) NotionScale() Notion

type Fraction6

type Fraction6 struct{}

func (Fraction6) NotionScale

func (Fraction6) NotionScale() Notion

type Fraction7

type Fraction7 struct{}

func (Fraction7) NotionScale

func (Fraction7) NotionScale() Notion

type Fraction8

type Fraction8 struct{}

func (Fraction8) NotionScale

func (Fraction8) NotionScale() Notion

type Fraction9

type Fraction9 struct{}

func (Fraction9) NotionScale

func (Fraction9) NotionScale() Notion

type Fraction10

type Fraction10 struct{}

func (Fraction10) NotionScale

func (Fraction10) NotionScale() Notion

type Fraction11

type Fraction11 struct{}

func (Fraction11) NotionScale

func (Fraction11) NotionScale() Notion

type Fraction12

type Fraction12 struct{}

func (Fraction12) NotionScale

func (Fraction12) NotionScale() Notion

type Fraction13

type Fraction13 struct{}

func (Fraction13) NotionScale

func (Fraction13) NotionScale() Notion

type Fraction14

type Fraction14 struct{}

func (Fraction14) NotionScale

func (Fraction14) NotionScale() Notion

type Fraction15

type Fraction15 struct{}

func (Fraction15) NotionScale

func (Fraction15) NotionScale() Notion

type Fraction16

type Fraction16 struct{}

func (Fraction16) NotionScale

func (Fraction16) NotionScale() Notion

type Fraction17

type Fraction17 struct{}

func (Fraction17) NotionScale

func (Fraction17) NotionScale() Notion

type Fraction18

type Fraction18 struct{}

func (Fraction18) NotionScale

func (Fraction18) NotionScale() Notion

type Fraction19

type Fraction19 struct{}

func (Fraction19) NotionScale

func (Fraction19) NotionScale() Notion

type Fraction20

type Fraction20 struct{}

func (Fraction20) NotionScale

func (Fraction20) NotionScale() Notion

type NativeUnit

type NativeUnit interface {
	comparable
	uint8 | uint16 | uint32 | uint64
}

NativeUnit is the subset backed by Go's native unsigned integer types.

type Notion

type Notion uint8

Notion is the fixed number of digits after the decimal point.

type PriceUint8

type PriceUint8[S VenueScale] struct {
	Uint8Units
}

PriceUint8 through PriceUint256 combine price identity and a fractional scale with an explicit scaled-integer backend. Backend width controls range; it is not inferred from fractional scale.

func (PriceUint8[S]) NotionScale

func (PriceUint8[S]) NotionScale() Notion

type PriceUint16

type PriceUint16[S VenueScale] struct {
	Uint16Units
}

func (PriceUint16[S]) NotionScale

func (PriceUint16[S]) NotionScale() Notion

type PriceUint32

type PriceUint32[S VenueScale] struct {
	Uint32Units
}

func (PriceUint32[S]) NotionScale

func (PriceUint32[S]) NotionScale() Notion

type PriceUint64

type PriceUint64[S VenueScale] struct {
	Uint64Units
}

func (PriceUint64[S]) NotionScale

func (PriceUint64[S]) NotionScale() Notion

type PriceUint256

type PriceUint256[S VenueScale] struct {
	Uint256Units
}

func (PriceUint256[S]) NotionScale

func (PriceUint256[S]) NotionScale() Notion

type Uint8Units

type Uint8Units struct{}

Uint8Units, Uint16Units, and Uint32Units are zero-sized unit providers. Embed one in a custom venue, or use the PriceUint* and AmountUint* formats.

type Uint16Units

type Uint16Units struct{}

type Uint32Units

type Uint32Units struct{}

type Uint64Units

type Uint64Units struct{}

Uint64Units is a zero-sized unit provider. Embed it in a venue type.

type Uint256Codec

type Uint256Codec struct {
	// contains filtered or unexported fields
}

Uint256Codec is the runtime-scale hot-path codec for scaled uint256 units.

Use Codec with a Venue when compile-time venue identity is required. Use Uint256Codec at boundaries where trusted metadata resolves the scale at runtime, such as CEX symbol decoding. Its methods avoid generic venue dispatch and return Error directly so successful and rejected parses remain allocation-free.

The zero value is a valid scale-0 codec. Scale is stored directly in one byte so repeated boundary operations do not decode constructor metadata.

Example
package main

import (
	"fmt"

	"github.com/JekaMas/sailfish"
	"github.com/holiman/uint256"
)

func main() {
	codec, err := sailfish.NewUint256Codec(18)
	if err != nil {
		fmt.Println(err)
		return
	}

	var units uint256.Int
	if parseErr := codec.ParseInto("1.250000000000000000", &units); parseErr != "" {
		fmt.Println(parseErr)
		return
	}

	fmt.Println(string(codec.AppendTo(nil, units)))
}
Output:
1.250000000000000000

func NewUint256Codec

func NewUint256Codec(scale Notion) (Uint256Codec, error)

NewUint256Codec validates scale once for repeated uint256 operations.

func (Uint256Codec) AppendCBOR

func (c Uint256Codec) AppendCBOR(dst []byte, units uint256.Int) []byte

AppendCBOR appends the preferred deterministic CBOR encoding for units. It allocates only when dst has insufficient capacity.

func (Uint256Codec) AppendTo

func (c Uint256Codec) AppendTo(dst []byte, units uint256.Int) []byte

AppendTo appends canonical fixed-scale text for units. It allocates only when dst has insufficient capacity.

func (Uint256Codec) CBORLen

func (c Uint256Codec) CBORLen(units uint256.Int) int

CBORLen returns the exact preferred deterministic CBOR size for units.

func (Uint256Codec) Len

func (c Uint256Codec) Len(units uint256.Int) int

Len returns the exact canonical text length for units.

func (Uint256Codec) MaxIntegerDigits

func (c Uint256Codec) MaxIntegerDigits() int

MaxIntegerDigits reports the maximum integer-part digit count at this scale.

func (Uint256Codec) Parse

func (c Uint256Codec) Parse(input string) (uint256.Int, Error)

Parse parses a strict non-negative decimal string into scaled units.

func (Uint256Codec) ParseBytes

func (c Uint256Codec) ParseBytes(input []byte) (uint256.Int, Error)

ParseBytes parses input without converting it to a string.

func (Uint256Codec) ParseBytesInto

func (c Uint256Codec) ParseBytesInto(input []byte, dst *uint256.Int) Error

ParseBytesInto parses input into dst without converting it to a string. It leaves dst unchanged on failure.

func (Uint256Codec) ParseCBOR

func (c Uint256Codec) ParseCBOR(raw []byte) (uint256.Int, Error)

ParseCBOR decodes preferred deterministic CBOR into scaled units.

func (Uint256Codec) ParseCBORFirst

func (c Uint256Codec) ParseCBORFirst(raw []byte) (uint256.Int, []byte, Error)

ParseCBORFirst decodes one preferred deterministic CBOR uint256 from the start of raw and returns the unconsumed suffix. It is intended for manual positional-array decoders that keep aggregate decoding allocation-free.

func (Uint256Codec) ParseCBORFirstInto

func (c Uint256Codec) ParseCBORFirstInto(raw []byte, dst *uint256.Int) ([]byte, Error)

ParseCBORFirstInto decodes one preferred deterministic CBOR uint256 into dst and returns the unconsumed suffix. It leaves dst unchanged on failure.

func (Uint256Codec) ParseCBORInto

func (c Uint256Codec) ParseCBORInto(raw []byte, dst *uint256.Int) Error

ParseCBORInto decodes preferred deterministic CBOR into dst. It leaves dst unchanged on failure.

func (Uint256Codec) ParseInto

func (c Uint256Codec) ParseInto(input string, dst *uint256.Int) Error

ParseInto parses input into dst. It leaves dst unchanged on failure.

func (Uint256Codec) Scale

func (c Uint256Codec) Scale() Notion

Scale returns the configured number of fractional decimal digits.

type Uint256Units

type Uint256Units struct{}

Uint256Units is a zero-sized unit provider. Embed it in a venue type.

type Unit

type Unit interface {
	comparable
	uint8 | uint16 | uint32 | uint64 | uint256.Int
}

Unit is the closed set of scaled-integer storage backends supported by Decimal.

type Venue

type Venue[U Unit] interface {
	VenueScale
	// contains filtered or unexported methods
}

Venue binds a fixed scale to one unit backend.

A custom venue is normally a zero-sized type. Prefer PriceUint* and AmountUint* when their semantic distinction applies:

type QuoteFraction5 struct{ sailfish.Uint64Units }
func (QuoteFraction5) NotionScale() sailfish.Notion { return 5 }

type VenueScale

type VenueScale interface {
	NotionScale() Notion
}

VenueScale supplies a fixed decimal scale. Implement it on a zero-sized value type with a value receiver.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL