hypercast

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Aug 31, 2026 License: MIT Imports: 10 Imported by: 0

README

hypercast

CI Go Reference

Go's own union idiom — (value, *Fault) — carrying the verdict of every cast: the value, or a closed reason plus the exact byte span that offended. *Fault implements error for composition, but the doors never panic on input; a panic here means a caller bug, never data.

Allocation-lean scalar casts — booleans, the full integer family, reals, UUIDs, temporals — calling directly into the native libhypercast Rust core. Two backends, chosen automatically by build tag, same public API either way: real cgo on darwin/linux (backend_cgo.go) — 3.5-4.8x faster per call, see Benchmarks — and purego (backend_purego.go) — dlopen/dlsym plus per-arch call trampolines, no cgo and no C compiler required — everywhere else, including Windows unconditionally and any darwin/linux build with CGO_ENABLED=0 (which, per Go's own defaults, includes every cross-compile). Bundles a native build for every supported platform via go:embed and picks the right one at runtime.

import "github.com/SkunkWerkx/HyperCast/go"

value, fault := hypercast.I32("(1,234)", hypercast.Invariant)
if fault != nil {
    log.Printf("%s at byte %d", fault.Reason, fault.Offset)
}
// value == -1234, accounting negative

ts, fault := hypercast.Timestamp("2026-01-02T15:04:05.123456789+05:00")
// a UTC time.Time at full nanosecond fidelity

Doors are generic over string | []byte — both cross zero-copy (the core only reads). Uuid returns google/uuid's uuid.UUID (RFC 9562 order is exactly its own layout). Go-flavored fidelity, stated honestly both ways: time.Time carries full nanoseconds across the whole 0001–9999 window and time-of-day comes back nanosecond-exact, but time.Duration's int64-nanosecond ceiling (±292 years) sits far below the core's ±10,000-year duration window, so Span returns the protobuf pair (Duration{Seconds, Nanos}) with a checked AsDuration() converter rather than silently wrapping.

Why not strconv / time.Parse?

  1. Verdicts with location — a closed reason plus the offending span, against strconv.NumError's wrapped string.
  2. The vocabulary untrusted sources actually send — twenty boolean lexemes, accounting parentheses, declared separators, radix prefixes, all five .NET Guid text forms plus urn:uuid: prefixes, protobuf JSON durations.
  3. One engine across a polyglot system — bit-for-bit verdicts with every other binding, held by the shared corpus (22 tests green on both backends, full twelve-file corpus replay).

The honest trade-off, stated as plainly as the wins elsewhere: every Go door loses per-call to Go's stdlib. Go's parsers are simply excellent (time.Parse(RFC3339Nano) at ~67 ns, strconv.Atoi at ~11 ns), and every HyperCast call pays a cgo crossing plus a structural heap allocation (any pointer crossing into opaque foreign code is excluded from escape analysis — a floor for this call shape, not an FFI-library choice). In Go specifically, this binding earns its keep on the vocabulary, the closed error contract, and cross-language agreement — not per-call speed. The batch/tabular layer (round three) is where the crossing amortizes to zero.

Benchmarks

go test -bench=. -benchmem ./... for cgo, the same with CGO_ENABLED=0 for purego. Measured on the same linux-arm64 machine, same run:

Door cgo purego stdlib
Timestamp 175 ns, 2 allocs 619 ns, 6 allocs 67 ns time.Parse(RFC3339Nano)
I32 177 ns, 3 allocs 613 ns, 7 allocs 11 ns strconv.Atoi
F64 206 ns, 3 allocs 617 ns, 7 allocs 39 ns strconv.ParseFloat
Uuid 148 ns, 2 allocs 599 ns, 6 allocs 35 ns google/uuid.Parse
Span (ISO) 172 ns, 2 allocs 623 ns, 6 allocs 71 ns ParseDuration (Go dialect — different grammar)
Bool 115 ns, 1 alloc 553 ns, 5 allocs 4 ns strconv.ParseBool
DateTime (1/7/2026 3:04 PM) 157 ns, 2 allocs 135 ns time.Parse w/ layout
DateOnlyOrdered (1/7/2026) 118 ns, 1 alloc 78 ns time.Parse w/ layout

Separator detection costs ~10 ns: 1.234.567,89 under Detect is 226 ns against 215 ns for the same text under a declared eurozone format (cgo backend).

cgo's 3.5-4.8x per-call win over purego is why it stays the default wherever it's available; purego's zero-toolchain story is why it carries Windows, CGO_ENABLED=0, and every cross-compile automatically. One caveat inherited with cgo-by-default: a native darwin/linux build on a machine with no C compiler at all (distroless-style container, macOS without Xcode CLT) now fails to build — CGO_ENABLED=0 go build ./... forces the purego fallback anywhere.

Install

go get github.com/SkunkWerkx/HyperCast/go

Go modules have no separate registry — go get resolves straight from a git tag, and because this module lives in a monorepo subdirectory its tags are prefixed (go/vX.Y.Z). The native libraries under native/{rid}/ are committed to git and kept fresh by stage-native-binaries.yml: a go:embed consumer has no packing step, so whatever is literally in the tree at the resolved tag is what gets embedded (see native/README.md).

See the repo root README for the full door table, the receipts, and the state of every other language binding.

Documentation

Overview

The cgo backend — HyperUuid's measured lesson applied from day one: purego's per-call trampoline allocations would eat scalar parsing alive (its batch API was 19.6x faster than per-call precisely because of that overhead), so scalar-per-call HyperCast runs on real C calls wherever cgo is available. backend_purego.go is the fallback half of the pair — Windows, CGO_ENABLED=0, and cross-compiles land there automatically.

cgo can't call an opaquely-typed void* function pointer directly — it needs a real, statically-typed C call site — hence the three per-signature shims below, one per ABI shape (plain, numeric, unix).

Package hypercast provides allocation-lean scalar casts — booleans, numerics, UUIDs, temporals — calling directly into the native libhypercast shared library. Every door returns Go's own union idiom: (value, *Fault), where a nil fault is the success case and a non-nil one carries the closed reason plus the offending byte span. Never an error for bad data in the exception sense — *Fault implements error for composition, but the doors never panic on input; a panic here means a caller bug (a malformed NumFormat), never data.

Door names mirror the native ABI (I32, F64, Timestamp, ...) so the polyglot surface reads identically across bindings. Doors are generic over string | []byte — both cross zero-copy (the core only reads).

Go-flavored fidelity, stated honestly both ways: time.Time carries full nanoseconds across the whole 0001–9999 window, and time-of-day comes back as a time.Duration since midnight, also nanosecond-exact. But time.Duration's int64-nanosecond ceiling (±292 years) sits far below the core's ±10,000-year duration window, so the duration door returns the protobuf pair (Duration{Seconds, Nanos}) with a checked AsDuration() converter rather than silently wrapping.

Index

Constants

This section is empty.

Variables

View Source
var Detect = NumFormat{DecimalSep: '.', GroupSep: ',', Styles: AllStyles | SeparatorDetect}

Detect is the detection profile — every lenience on, './,' roles resolved per input by SeparatorDetect's structural rules.

View Source
var Invariant = NumFormat{DecimalSep: '.', GroupSep: ',', Styles: AllStyles}

Invariant is the invariant profile — '.' decimal, ',' grouping, every lenience on.

Functions

func DateOnly

func DateOnly[T Text](text T) (Date, *Fault)

DateOnly casts a strict ISO 8601 yyyy-MM-dd calendar date.

func DateOnlyOrdered

func DateOnlyOrdered[T Text](text T, order DateOrder) (Date, *Fault)

DateOnlyOrdered casts a separated calendar date — three digit fields joined by one consistent separator (/, -, or .) — under the caller-declared DateOrder. The year field is four digits wherever the order puts it (two-digit years mean century guessing, which never happens here); an undefined order is a caller bug and panics, never a verdict. The strict DateOnly door keeps rejecting every separated form.

func DateTime

func DateTime[T Text](text T, order DateOrder) (CivilDateTime, *Fault)

DateTime casts a zone-less civil date-time — the shape untrusted feeds actually send ("1/7/2026 3:04 PM", "2026-01-07 15:04:05") — under the caller-declared DateOrder. The date part follows DateOnlyOrdered's grammar; the optional time part (one space or 'T' after the date) is 24-hour h:mm[:ss[.f]] or 12-hour with an AM/PM marker; absent, the time is midnight. No zone is read and none is invented — Timestamp stays the strict RFC 3339 instant door. An undefined order is a caller bug and panics, never a verdict.

func Span

func Span[T Text](text T) (Duration, *Fault)

Span casts a duration (ISO 8601 fixed components, invariant colon form, or protobuf JSON seconds) to the protobuf pair — see Duration for why not time.Duration directly.

Types

type CastFailure

type CastFailure int32

CastFailure is the closed set of reasons a cast can fail — the native core's verdict codes, verbatim.

const (
	// Empty means required input was empty or whitespace.
	Empty CastFailure = 1
	// Malformed means input was present but not recognizable as the target type.
	Malformed CastFailure = 2
	// OutOfRange means well-formed but outside the target's range — "256" for a U8,
	// 1e400 for an F64.
	OutOfRange CastFailure = 3
)

func (CastFailure) String

func (f CastFailure) String() string

type CivilDateTime

type CivilDateTime struct {
	Date      Date
	TimeOfDay time.Duration
}

CivilDateTime is a wall-clock date and time with no zone — exactly what zone-less text like "1/7/2026 3:04 PM" actually names. Deliberately not a time.Time: without a zone there is no instant, and inventing one (assuming UTC, say) would be a silent value error of up to ±14 hours. Fuse a zone yourself when you know it: time.Date(d.Date.Year, d.Date.Month, d.Date.Day, 0, 0, 0, 0, loc).Add(d.TimeOfDay).

type Date

type Date struct {
	Year  int
	Month time.Month
	Day   int
}

Date is a calendar date with no time or zone — the protobuf google.type.Date fields. Go has no standard date-only type; this keeps the core's digits exactly.

type DateOrder

type DateOrder uint32

DateOrder is the caller-declared field order of a separated calendar date — no guessing, ever: "1/7/2026" is January 7th (MonthDayYear, the en-US order) or July 1st (DayMonthYear, the en-GB order) only because the caller said which.

const (
	YearMonthDay DateOrder = 1
	MonthDayYear DateOrder = 2
	DayMonthYear DateOrder = 3
)

The declared field orders — the native core's codes, verbatim.

type Duration

type Duration struct {
	Seconds int64
	Nanos   int32
}

Duration is the protobuf pair — whole seconds plus same-signed nanoseconds. Returned instead of time.Duration because the core's ±10,000-year window exceeds time.Duration's int64-nanosecond ceiling (±292 years); AsDuration converts when the value fits.

func (Duration) AsDuration

func (d Duration) AsDuration() (time.Duration, bool)

AsDuration converts to time.Duration, reporting false when the value exceeds time.Duration's ±292-year representable range.

type ExcelEpoch

type ExcelEpoch uint32

ExcelEpoch is the date system an Excel serial number is expressed in. Spreadsheets carry no marker for this — it is a workbook-level setting — so the caller states it, the same way UnixPrecision and DateOrder are declared rather than guessed.

const (
	// Excel1900 is the Windows default: serial 1 is 1900-01-01, and serial 60 is a
	// February 29th that never existed.
	Excel1900 ExcelEpoch = 1
	// Excel1904 is the legacy Macintosh system, still selectable today: serial 0 is
	// 1904-01-01, with no phantom day anywhere in it.
	Excel1904 ExcelEpoch = 2
)

The declared Excel date systems — the native core's codes, verbatim.

type Fault

type Fault struct {
	Reason CastFailure
	Offset int
	Length int
}

Fault is the failure case of a cast: a closed reason plus the offending span, as byte offsets into the input (identical to what the door received — string and []byte inputs are both raw UTF-8 here). Implements error so verdicts compose with Go's error plumbing, but nothing is captured or formatted until Error() is actually called.

func Bool

func Bool[T Text](text T) (bool, *Fault)

Bool casts boolean text: true/false plus the conventions untrusted sources actually send (t/f, yes/no, y/n, 1/0, on/off, enabled/disabled, active/inactive, checked/unchecked, in/out), ASCII case-insensitive.

func ExcelSerial

func ExcelSerial[T Text](text T, epoch ExcelEpoch) (time.Time, *Fault)

ExcelSerial casts an Excel date serial under a caller-declared ExcelEpoch to a UTC time.Time. The whole part counts days from the system's own day zero and the fraction is the time of day, so "45292.75" is 2024-01-01T18:00:00Z. A spreadsheet cell carries no zone and none is invented.

The 1900 system contains a day that never existed: serial 60 is 1900-02-29, kept deliberately because Lotus 1-2-3 wrongly treated 1900 as a leap year and Excel copied the bug for file compatibility. It is Malformed here — the same verdict DateOnly gives the text "1900-02-29" — so every serial above it is shifted one day against a naive count, which is the arithmetic hand-rolled conversions get wrong.

An undefined epoch is a caller bug and panics, never a verdict.

func F32

func F32[T Text](text T, format NumFormat) (float32, *Fault)

F32 casts real text under the declared format: finite values only (NaN/Infinity literals are Malformed, overflow to infinity is OutOfRange), declared separators and grouping, accounting parentheses, exponent, and trailing percent (50% is 0.5).

func F64

func F64[T Text](text T, format NumFormat) (float64, *Fault)

F64 casts real text to float64. Rules as F32.

func I8

func I8[T Text](text T, format NumFormat) (int8, *Fault)

I8 casts integer text under the declared format: the type's own range, declared grouping, accounting parentheses, non-negative exponent (1e3 is 1000; a decimal point is never accepted), and 0x/&H/0b two's-complement radix prefixes (0xFF is -1). The other integer doors share these rules at their own widths.

func I16

func I16[T Text](text T, format NumFormat) (int16, *Fault)

I16 casts integer text to int16. Rules as I8.

func I32

func I32[T Text](text T, format NumFormat) (int32, *Fault)

I32 casts integer text to int32. Rules as I8.

func I64

func I64[T Text](text T, format NumFormat) (int64, *Fault)

I64 casts integer text to int64. Rules as I8.

func TimeOfDay

func TimeOfDay[T Text](text T) (time.Duration, *Fault)

TimeOfDay casts an ISO 24-hour time-of-day to a time.Duration since midnight — nanosecond-exact, and comfortably inside time.Duration's range.

func Timestamp

func Timestamp[T Text](text T) (time.Time, *Fault)

Timestamp casts an RFC 3339 instant — zone mandatory — to a UTC time.Time at full nanosecond fidelity across the whole 0001–9999 window.

func U8

func U8[T Text](text T, format NumFormat) (uint8, *Fault)

U8 casts integer text to uint8. Rules as I8.

func U16

func U16[T Text](text T, format NumFormat) (uint16, *Fault)

U16 casts integer text to uint16. Rules as I8.

func U32

func U32[T Text](text T, format NumFormat) (uint32, *Fault)

U32 casts integer text to uint32. Rules as I8.

func U64

func U64[T Text](text T, format NumFormat) (uint64, *Fault)

U64 casts integer text to uint64 — natively unsigned, no widening games. Rules as I8.

func Unix

func Unix[T Text](text T, precision UnixPrecision) (time.Time, *Fault)

Unix casts an integer Unix-epoch value under a caller-declared unit to a UTC time.Time. An undefined precision is a caller bug and panics, never a verdict.

func Uuid

func Uuid[T Text](text T) (uuid.UUID, *Fault)

Uuid casts UUID text — all five .NET Guid formats (D/N/B/P/X) plus urn:uuid:/GUID:/UUID: prefixes — to a uuid.UUID (RFC 9562 byte order, which is exactly uuid.UUID's own layout).

func (*Fault) Error

func (f *Fault) Error() string

type NumFormat

type NumFormat struct {
	DecimalSep rune
	GroupSep   rune
	Styles     NumStyles
}

NumFormat is the caller-declared numeric notation for the integer and real doors. The core carries no culture data — a call site parsing culture-sensitive text declares its format out loud (Invariant, or a literal); there is no default, the same stance every binding in this repo takes. Equal separators are a caller bug and panic, never a verdict.

type NumStyles

type NumStyles uint32

NumStyles are the lenience flags of NumFormat — bit-for-bit the native core's flags.

const (
	// Grouping permits the group separator between digits (sizes not validated — between
	// digits is the rule).
	Grouping NumStyles = 1 << iota
	// Parentheses permits accounting negation: (1,234) is -1234.
	Parentheses
	// Exponent permits exponent notation. Integer doors reject a negative exponent.
	Exponent
	// RadixPrefixes permits 0x/&H/0b two's-complement prefixes (0xFF is -1 for an I8).
	RadixPrefixes
	// Percent permits a trailing %, dividing by 100. Real doors only.
	Percent
	// SeparatorDetect resolves the './,' roles per input from structure instead of the
	// declared separators (which are ignored while this flag is set). Detection, not
	// sniffing: a repeated separator is grouping (1.234.567,89); with both present the
	// rightmost is the decimal; a single separator with a non-3-digit right run is the
	// decimal (3,1415); with exactly 3 digits right, only a 0 integer part proves decimal
	// (0,785). Genuinely ambiguous input (12.185, 1,000) is Malformed at the separator,
	// never guessed.
	SeparatorDetect NumStyles = 1 << 5

	// AllStyles is every lenience on.
	AllStyles = Grouping | Parentheses | Exponent | RadixPrefixes | Percent
)

type Text

type Text interface {
	string | []byte
}

Text constrains the door inputs: Go's two spellings of raw UTF-8.

type UnixPrecision

type UnixPrecision uint32

UnixPrecision is the declared unit of a Unix-epoch value — no magnitude guessing, ever.

const (
	Seconds      UnixPrecision = 1
	Milliseconds UnixPrecision = 2
	Microseconds UnixPrecision = 3
	Nanoseconds  UnixPrecision = 4
)

The declared units a Unix-epoch value can carry — the native core's codes, verbatim.

Jump to

Keyboard shortcuts

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