Help us shape the future of Go! Take the 2026 Go Developer Survey

hypercast

package module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Oct 25, 2026 License: MIT Imports: 9 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, characters, the full integer family, reals, exact decimals, UUIDs, temporals — calling directly into the native hypercast Rust core, linked into your binary through cgo (backend_static.go). The core is a static library on the link line: nothing is embedded, nothing is extracted, nothing is dlopened, nothing can fail to load, and the binary runs from a read-only filesystem or a scratch image. go get is the whole install; a C compiler at build time is the one requirement (see Requirements). TinyGo links the same core into WebAssembly, browser included.

import hypercast "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

Install

go get github.com/SkunkWerkx/HyperCast/go@latest

Requires Go 1.26 or later (the go directive in go.mod). The import path ends in /go while the package is named hypercast, so spell the name out in the import, as above — goimports adds it for you, but a hand-written import "github.com/SkunkWerkx/HyperCast/go" reads as if the package were called 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/v<version>). The core's static libraries under staticlib/ are committed to git and restaged for each release by stage-native-binaries.yml: a go get consumer has no packing step, so whatever is literally in the tree at the resolved tag is what gets linked (see staticlib/README.md). github.com/google/uuid is the module's only dependency.

Requirements

cgo, and so a C compiler wherever the module is built — nothing at run time:

Building on / for C compiler
Linux x64 / arm64, glibc or musl gcc or clang (build-essential; on Alpine, apk add build-base)
macOS x64 / arm64 the Xcode command-line tools (xcode-select --install)
Windows x64 MinGW-w64 gcc
Windows arm64 llvm-mingw
iOS, the iOS simulator, Mac Catalyst Xcode's clang for that platform's SDK; see iOS and Mac Catalyst
Android arm64 / x64 the NDK's clang for that ABI as CC; see Android

Every other build fails at compile time, by name:

undefined: hypercast_needs_cgo_and_a_C_compiler_on_linux_darwin_or_windows_amd64_arm64__set_CGO_ENABLED_1__for_wasm_build_with_TinyGo

That is CGO_ENABLED=0 (which is also Go's default for a cross-compile — see Building and cross-compiling), any OS or architecture outside the ones above (the iOS simulator on an Intel Mac among them, which Go's own rules would otherwise count as macOS), and stock Go compiled to WebAssembly (GOOS=wasip1, GOOS=js): Go's wasm toolchain links Go code only, with no cgo, so a foreign library has nowhere to go. For WebAssembly, build with TinyGo, which links the core there, browser included.

In the browser (TinyGo)

TinyGo 0.42 or later compiles this module to WebAssembly with the core linked in, the way the C# package does for Blazor: TinyGo links with wasm-ld and has cgo, so backend_tinygo.go names staticlib/wasm/libhypercast.a — the core built for wasm32-wasip1 — on its link line. No code changes and no extra files to ship: the core is inside your .wasm.

tinygo build -target=wasm -no-debug -opt=z -o main.wasm .
cp "$(tinygo env TINYGOROOT)/targets/wasm_exec.js" .
<script src="wasm_exec.js"></script>
<script>
  const go = new Go();
  WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject)
    .then(result => go.run(result.instance));
</script>

Use TinyGo's own wasm_exec.js, not stock Go's. The core itself imports nothing — parsing needs no clock, no randomness and no I/O — so the page loads one module and that is all. -target=wasip1 works the same way under a WASI runtime (wasmtime, for one). CI builds internal/tinygosmoke this way on every pull request and runs it in headless Chrome: every ABI shape — plain, numeric with a format (a currency symbol, and every other symbol the layout carries), discriminated, the civil date-time door with declared day periods — a fault with its span, and the core's version against rust/Cargo.toml.

TinyGo's cgo is narrower than Go's — no build constraints on #cgo lines, no ${SRCDIR}, no C structs by value — which is why the browser build has a backend file of its own rather than more lines in backend_static.go; the file's header has the details. The one that changes how a call crosses is the last: backend_static.go's shims hand the verdict back by value, so under TinyGo each door instead passes the core pointers to its result's out-value and fault span, the way the core's exports take them anyway.

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

Doors

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.

Door Returns Declares
Bool bool —
Char rune — one character verbatim, or a declared code point (U+00E9, 0xE9, &H41, &#233;, &#x41;, decimal 233) —
I8 I16 I32 I64 U8 U16 U32 U64 the Go integer NumFormat
F32 F64 float32 / float64 NumFormat
Exact Decimal — sign, 96-bit magnitude, scale 0..=28 NumFormat
Uuid uuid.UUID —
Timestamp UTC time.Time —
Unix UTC time.Time UnixPrecision
ExcelSerial UTC time.Time ExcelEpoch
DateOnly Date —
DateOnlyOrdered Date DateOrder
DateTime CivilDateTime DateOrder
DateTimeWith CivilDateTime DateFormat — the order plus the culture's AM/PM designators
TimeOfDay time.Duration since midnight —
Span Duration{Seconds, Nanos} —
ExactFromFloat64 Decimal, the shortest that names the float64 (0.1 is one tenth) —
ExcelSerialFromFloat64 CivilDateTime ExcelEpoch
ExcelTime time.Duration since midnight —
ExcelDuration Duration{Seconds, Nanos} —

Plus three entry points that are not doors — Available(), LoadError() and NativeVersion() — covered under The native library below.

Numeric — one door generic over the target

A caller that is itself generic over the target type would otherwise write the eleven-way door switch. Numeric[V Number, T Text] writes it once: Number is the closed set of types the numeric doors return — int8 … int64, uint8 … uint64, float32, float64, Decimal — and Numeric[int32] is I32, Numeric[Decimal] is Exact, verdict for verdict. T is inferred from the argument. An unsupported V is impossible by construction: the constraint is exactly the door list, so the compiler rejects anything else at the call site.

func column[V hypercast.Number](cells []string, format hypercast.NumFormat) ([]V, *hypercast.Fault) {
    out := make([]V, 0, len(cells))
    for _, cell := range cells {
        v, fault := hypercast.Numeric[V](cell, format)
        if fault != nil {
            return nil, fault
        }
        out = append(out, v)
    }
    return out, nil
}
Scalar — one door generic over every target

Scalar[V ScalarTarget, T Text] widens Numeric to every target with a door of its own: each Number, plus bool (Bool), uuid.UUID (Uuid), time.Time (Timestamp — RFC 3339, zone mandatory, normalized to UTC), Date (DateOnly, strict yyyy-MM-dd) and Duration (Span). The NumFormat argument is read by the numeric doors only. The verdicts are the concrete door's, and as with Numeric an unsupported V fails to compile. Three types are left out on purpose: rune is int32, so Scalar[int32] is the integer door and a character is Char's; time.Duration is what TimeOfDay returns, but every Go reader takes it for a span; and CivilDateTime needs a declared DateOrder.

v, fault := hypercast.Scalar[time.Time]("2026-01-02T15:04:05+05:00", hypercast.Invariant)
Exact decimals

Exact is the decimal door — named that way because the result type already owns the identifier Decimal, the same reason Span returns a Duration. It reads the real doors' grammar under the same NumFormat but never rounds: Decimal{Lo, Hi, Scale, Negative} is the .NET decimal shape, (−1)^Negative × (Hi·2⁶⁴ + Lo) × 10⁻ˢᶜᵃˡᵉ, and text carrying more than 96 bits or 28 places is OutOfRange, not approximated. The result is canonical: exact trailing zeros in the fraction are trimmed so the scale is minimal, so "1.10", "1.1" and "1.1000" are all magnitude 11 at scale 1 and String() renders each as "1.1"; Rat() hands over the exact value as a *big.Rat. Zero is never negative and always scale 0.

d, fault := hypercast.Exact("($1,234.50)", hypercast.NumFormat{
    DecimalSep: ".", GroupSep: ",", Styles: hypercast.AllStyles, Currency: "$",
})
// d == Decimal{Lo: 12345, Scale: 1, Negative: true}; d.String() == "-1234.5"
NumFormat: separators, currency, and the culture's own symbols

Every numeric door takes a NumFormat — the caller's declared notation, never a guess. Go's standard library carries no culture data, so there is no From(culture) bridge here as the C#, Java and Swift bindings have; the struct literal is the whole API, and an application fills it from whatever culture data it holds. Invariant is "."/"," with AllStyles and nothing else declared; Detect adds SeparatorDetect. AllStyles is every lenience including CurrencySymbol (and excluding SeparatorDetect and TrailingSign), so keyed literals that predate a field keep compiling and keep their meaning.

Field Declares Read
DecimalSep, GroupSep the number separators — 1 to 8 UTF-8 bytes each with no ASCII digit (".", ",", " ", ", "), different from each other; the only required fields always
Styles the NumStyles leniences —
Currency the currency symbol ("$", "€", "kr."), at most 16 bytes under CurrencySymbol
CurrencyDecimalSep, CurrencyGroupSep the separators of a currency amount, where a culture points and groups money differently from numbers; empty keeps the number separator when Currency is declared and found in the text; ignored under SeparatorDetect, whose detection governs the whole token
MinusSign, PlusSign a further spelling of the sign ("−", U+2212, in Estonian and Basque text) wherever ASCII -/+ are
Percent a further spelling of the percent symbol ("٪", "%") as a trailing % is, under Percent — the real and decimal doors; the integer doors never read a percent
DigitZero native digits: the ten code points from this zero up ('٠' for the Arabic-Indic digits, '0' for fullwidth); 0 declares none wherever a digit is — integer part, fraction, exponent; a 0x/0b body stays ASCII

The rule for every field past the separators: the ASCII spelling is always read, and a declared symbol is a further spelling read beside it. Nothing that parsed before a symbol was declared stops parsing once it is: "-5" is -5 under a format declaring "−", and so is "−5"; "50%" is 0.5 under a format declaring "٪", and so is "50٪"; "123" is 123 under a format declaring '٠', and so are "١٢٣" and "12٣". Two signs in any spelling are Malformed at the later one. Under CurrencySymbol with a Currency declared, the symbol is accepted once, whole, at either edge of the numeric body: leading, before or after a sign ($5, -$5, $ -5, €−5), or trailing (5 €, 1.234,50 kr.), with optional ASCII whitespace between symbol and digits; accounting parentheses wrap symbol and digits together (($5)). It never takes part in the digit scan, so a symbol containing a separator character (kr. under . grouping) is fine. A symbol declared while the style is off is Malformed at the symbol; the style with no symbol declared is a no-op.

TrailingSign is the one style that is not in AllStyles: it permits the sign after the digits (5-, 5 -, 5+, 5 €-, 5- €, 2.5%-), in ASCII or the declared spelling, once, never beside a leading sign and never inside parentheses. A culture whose negative pattern is n- or n - sets it; everything else leaves it off, so "5-" stays Malformed under Invariant.

A format the core would refuse is a caller bug and panics, never a verdict — the way equal separators always have: a number separator that is empty or longer than 8 bytes, equal number separators or equal separators in force for a currency amount, any symbol that is not valid UTF-8 or carries an ASCII digit, a sign, percent or currency symbol carrying ASCII whitespace, a Currency longer than 16 bytes, or a DigitZero that is ASCII or starts no Unicode digit block. NumFormat.Raw() reports the same check as an error for code that would rather not panic.

estonian := hypercast.NumFormat{
    DecimalSep: ",", GroupSep: " ", Styles: hypercast.AllStyles | hypercast.TrailingSign,
    Currency: "€", MinusSign: "−",
}
value, fault := hypercast.F64("−1 234,5 €", estonian) // -1234.5; so is "1 234,5 €−", and "-1234,5"

arabic := hypercast.NumFormat{DecimalSep: "٫", GroupSep: "٬", Styles: hypercast.AllStyles, DigitZero: '٠', Percent: "٪"}
value, fault = hypercast.F64("١٢٫٥٪", arabic) // 0.125

euros := hypercast.NumFormat{DecimalSep: ",", GroupSep: ".", Styles: hypercast.AllStyles, Currency: "€"}
value, fault = hypercast.F64("€ 1.234,50", euros) // 1234.5

The boolean lexicon stays English (true/yes/on/enabled/… and their negatives): no platform has culture data for it, so no binding bridges one — a caller maps a culture's words before the door.

DateFormat: the culture's day periods

DateTime reads a 12-hour clock's marker as the English AM/PM, ASCII case-insensitively, after the clock or ahead of it. DateTimeWith takes a DateFormat instead of the bare DateOrder — the same declarative shape as NumFormat: the order, and the words the culture writes for the two halves of the day. A declared designator is matched byte for byte, after the clock (3:04 nachm., optional space before) or ahead of it (오후 3:04, optional space after), and the English pair is read whatever is declared. A DateFormat declaring no designators reads exactly what DateTime reads, and DateTime itself is unchanged. Only the civil date-time door reads them: DateOnlyOrdered has no time part.

A designator is at most 16 UTF-8 bytes, carries no ASCII digit, and neither starts nor ends in ASCII whitespace (an inner space, as in Spanish a. m., is fine); anything else is a caller bug and panics, and DateFormat.Raw() reports it as an error.

german := hypercast.DateFormat{Order: hypercast.DayMonthYear, AM: "vorm.", PM: "nachm."}
civil, fault := hypercast.DateTimeWith("1.7.2026 3:04 nachm.", german) // July 1st, 15:04

korean := hypercast.DateFormat{Order: hypercast.YearMonthDay, AM: "오전", PM: "오후"}
civil, fault = hypercast.DateTimeWith("2026-07-01 오후 3:04", korean) // the same wall clock

The native library: Available, LoadError, NativeVersion

The core is part of the binary, so there is nothing to load and nothing that can fail, and no door can panic for want of the library. The three probes every binding carries are here so code written against them works unchanged:

  • Available() — always true.
  • LoadError() — always nil.
  • NativeVersion() — "major.minor.patch" as the linked core reports it about itself, read through the ABI rather than from this module, so a deployment can confirm which build of the archive went into the binary. It cannot fail.
log.Printf("hypercast core %s", hypercast.NativeVersion())

HyperUuid's Go binding has the same three entry points.

Interop: building on HyperCast's C ABI

For a module that carries HyperCast's verdicts across a C ABI of its own (HyperTabular does): the layouts the core writes, the conversions every door applies to them, the checked codes of the declared options, and the packed version word — the same code the doors run, so a value read out of another library's buffer is the value the door of the same name would have returned.

  • RawTimestamp (16 bytes) → .Time(), RawDate (4) → .Date(), RawCivil (16) → .CivilDateTime(). Decimal and Duration need no raw twin: each is laid out exactly as the core writes one, checked at compile time, so a buffer of them can be viewed as a []Decimal or []Duration in place.
  • RawNumFormat — the 92 bytes a format crosses as, which the core reads in place: a size word, the flags, the digit zero's code point, then every symbol inline as its bytes followed by its length — RawSymbol8 (9 bytes; SymbolMaxBytes is 8) for the separators, signs and percent symbol, RawSymbol16 (17 bytes; CurrencyMaxBytes is 16) for the currency symbol. NumFormat.Raw() builds one, returning the doors' check as an error instead of their panic.
  • RawDayPeriods — the 40 bytes a DateFormat's designators cross as beside the order: a size word and the AM and PM designators as RawSymbol16s (DesignatorMaxBytes is 16); a zero size is what crosses as null, nothing declared. DateFormat.Raw() builds one.
  • Valid() on UnixPrecision, DateOrder and ExcelEpoch; UnixPrecisionFromCode, DateOrderFromCode, ExcelEpochFromCode and ReasonFromCode turn an ABI code back into the type, and false for any other value.
  • FaultFromCode(code, offset, length) — the *Fault a nonzero verdict code and its span name; false when the code names no reason, a binding bug rather than data.
  • FormatVersion(packed) — a *_version() export's major<<16 | minor<<8 | patch word as "major.minor.patch".
raw, err := hypercast.NumFormat{DecimalSep: ",", GroupSep: ".", Styles: hypercast.AllStyles, Currency: "€"}.Raw()
// raw crosses to the other library's C ABI; its verdicts come back as codes and spans:
fault, ok := hypercast.FaultFromCode(code, offset, length)

Platforms

Platform Archive linked Also links
Linux x64 / arm64, glibc and musl (Alpine) staticlib/linux_amd64, staticlib/linux_arm64 the C library
macOS x64 / arm64 staticlib/darwin_amd64, staticlib/darwin_arm64 the C library
Windows x64 / arm64 staticlib/windows_amd64, staticlib/windows_arm64 nothing (the C runtime)
iOS arm64 — device, simulator staticlib/ios_arm64, staticlib/iossimulator_arm64 the C library
Mac Catalyst arm64 / x64 staticlib/maccatalyst_arm64, staticlib/maccatalyst_amd64 the C library
Android arm64 / x64 (API 21+) staticlib/android_arm64, staticlib/android_amd64 Bionic, the C library
WebAssembly under TinyGo — browser, WASI staticlib/wasm nothing

Each build names one archive on its link line and that is all it takes from this module: the binary carries the core for its own platform, writes nothing to disk, and starts without touching the filesystem.

One archive serves glibc and musl. cgo has no build constraint that tells the two C libraries apart, so the Linux archives are the core built for the musl target, which asks the C library for nothing glibc and musl do not both have (memcpy, memset, bcmp).

Windows links the MSVC archive, the same bytes the C# package's Native AOT publish links. MinGW's linker reads MSVC's COFF objects, and what the archive asks of the C runtime resolves against msvcrt.dll through MinGW's own import library, so the link line names nothing else.

iOS and Mac Catalyst

Go builds for all three as GOOS=ios, with cgo and the platform's own clang, which is what gomobile arranges and what Go's own misc/ios/clangwrap.sh does for the simulator. A Mach-O object says which platform it was built for and the linker refuses a mismatch, so each has its own archive, and since the three are one GOOS/GOARCH pair to Go, build tags choose between them:

Building for Tags Archive
An iOS device none staticlib/ios_arm64
The iOS simulator, Apple silicon iossimulator staticlib/iossimulator_arm64
Mac Catalyst maccatalyst staticlib/maccatalyst_arm64, staticlib/maccatalyst_amd64

gomobile sets maccatalyst itself for that target. Nothing sets iossimulator: gomobile builds a device and a simulator with the same tags, so a simulator build passes -tags iossimulator by hand, and a build that omits it links the device archive and fails at the simulator link. A gomobile bind covering both in one invocation gives them one set of tags, so they take two invocations. The simulator on an Intel Mac has no archive and is a compile error.

CI checks that every platform and tag combination selects its own archive (.github/scripts/check_go_archives.sh), runs this suite in an iOS simulator through Go's misc/ios/go_ios_exec.go, with the corpus staged as testdata/corpus since that wrapper carries nothing above the module into the simulator, and links a device build. Mac Catalyst is not linked from Go there; its archives are the ones the Swift and C# bindings link and run in the same job.

Android

GOOS=android with cgo, the NDK's clang as CC, as gomobile arranges and as any cgo package on Android needs:

CC=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang \
  GOOS=android GOARCH=arm64 CGO_ENABLED=1 go build ./...

(x86_64-linux-android21-clang and GOARCH=amd64 for the emulator.) GOOS=android also satisfies Go's linux constraint, so the Linux link lines exclude it and it takes its own archives, built for Android against Bionic. Page alignment is the final link's: NDK r28 and later align to the 16 KB pages Android 15 devices may use by default, and an older NDK needs -extldflags=-Wl,-z,max-page-size=16384.

CI checks that both select their own archive (.github/scripts/check_go_archives.sh), cross-compiles this whole suite for android/amd64, and runs it in an x86_64 emulator whose image uses 16 KB pages (.github/scripts/android_build_suite.sh and android_device_test.sh, which run the same way against a local emulator); android/arm64 is linked.

Deploying
  • A fully static Linux binary — go build -ldflags '-linkmode external -extldflags -static' gives a binary with no dependencies at all, which runs in an empty, read-only scratch container. It needs a static C library to link against; Alpine's musl has one.
  • Shipping to Alpine — build on Alpine (apk add build-base), or link fully statically as above. An ordinary dynamically linked build from a glibc machine needs the builder's glibc, which a bare Alpine image does not have.
  • Anywhere else — the binary needs only the C library it was linked against.

Building and cross-compiling

CGO_ENABLED defaults to 1 on a native build and to 0 the moment GOOS/GOARCH differ from the host:

$ go env CGO_ENABLED                # native
1
$ GOARCH=arm64 go env CGO_ENABLED   # cross, same OS, different arch
0
$ GOOS=windows go env CGO_ENABLED   # cross, different OS
0

So a plain cross-compile lands on the compile error above. Cross-compiling takes a cross C compiler and CGO_ENABLED=1 set explicitly:

CC=x86_64-w64-mingw32-gcc GOOS=windows GOARCH=amd64 CGO_ENABLED=1 go build ./...
CC=aarch64-linux-gnu-gcc  GOOS=linux   GOARCH=arm64 CGO_ENABLED=1 go build ./...

A native build with no C compiler installed fails in runtime/cgo with C compiler "gcc" not found — install the one for your platform from Requirements. GitHub's ubuntu-latest and macos-latest runner images ship one by default (gcc and the Xcode command-line tools' clang).

The hypercast_local build tag is for working on the core in a checkout of this repository: go test -tags hypercast_local ./... links the archive .github/scripts/local-core.sh builds from the checkout, under rust/target/local-core/, in place of the committed one. A module fetched with go get has no such archive, so the tag fails at link time there. Without the tag, outside CI, the suite refuses to run when the checkout's rust/ differs from the commit the committed archives were built from (-count=1 after a core change, since go test replays a cached pass).

This repo's own CI runs go test ./... natively, never cross-compiled, on every leg — Linux and Windows on x64 and arm64, macOS on arm64 — and on Alpine. The iOS simulator run in test-apple-mobile and the Android emulator run in test-android are the two exceptions; see iOS and Mac Catalyst and Android.

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, signs, currency symbols, percent symbols and native digits, a culture's own AM/PM, 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 (full corpus replay on every platform, and in the browser under TinyGo).

The honest trade-off, stated as plainly as the wins elsewhere: every Go door but the two declared-order date doors loses per-call to Go's stdlib. Go's parsers are simply excellent (time.Parse(RFC3339Nano) at ~58 ns, strconv.Atoi at ~9 ns), and every HyperCast call pays a cgo crossing of about 50 ns. The exceptions are the messy date-time door, which beats time.Parse with a layout by 1.4x, and the declared-order date door, which edges it: time.Parse with a layout is the one stdlib path slow enough to absorb the crossing. The crossing is all a door pays: any Go pointer handed to cgo escapes to the heap, so the format and the day periods cross by value, the C shims in backend_static.go declare them, the out-value and the fault span on their own stack and return the verdict by value as one struct, and no Go pointer crosses but the input bytes — 0 B, 0 allocs on every door on the success path. (A failed cast allocates exactly its *Fault; allocs_test.go holds both counts as tests.) In Go specifically, this binding earns its keep on the vocabulary, the closed error contract, and cross-language agreement — not per-call speed. The tabular layer, HyperTabular, is where the crossing amortizes to zero: it crosses once per chunk, not once per cell.

Benchmarks

go test -bench=. -benchmem ./.... Measured for 0.9.0 on linux-x64 (an Intel Core i9-11900H, go1.27.2), one session, median of five runs:

Door HyperCast stdlib
Timestamp 90 ns, 0 allocs 58 ns time.Parse(RFC3339Nano)
I32 103 ns, 0 allocs 9 ns strconv.Atoi
I32 (grouped) 108 ns, 0 allocs —
F64 121 ns, 0 allocs 30 ns strconv.ParseFloat
Uuid 76 ns, 0 allocs 33 ns google/uuid.Parse
Span (ISO) 78 ns, 0 allocs 73 ns ParseDuration (Go dialect — different grammar)
Bool 60 ns, 0 allocs 3 ns strconv.ParseBool
TimeOfDay 69 ns, 0 allocs —
DateTime (1/7/2026 3:04 PM) 84 ns, 0 allocs 118 ns time.Parse w/ layout
DateOnlyOrdered (1/7/2026) 67 ns, 0 allocs 72 ns time.Parse w/ layout

Separator detection costs ~16 ns: 1.234.567,89 under Detect is 141 ns against 125 ns for the same text under a declared eurozone format.

A format is checked and laid out on every call — the two separators in a few nanoseconds, every further symbol a little more — and the whole 92-byte layout crosses by value, so a declared symbol costs text that does not carry it: I32("1,234,567") is 108 ns under Invariant, 121 ns under a format declaring $, and 204 ns under one declaring every symbol there is (currency separators, both signs, percent, native digits). Declare what the feed actually carries. Day periods are the same story: DateTime("1/7/2026 3:04 PM") is 84 ns, and DateTimeWith under a format declaring vorm./nachm. reads the same text in 135 ns. Rejection allocates where success does not: I32("1,234x567") takes 171 ns and one allocation, the *Fault it returns, against 49 ns and two allocations for strconv.Atoi failing on 1234x567.

Verifying build provenance

Go has no package registry to attest: go get resolves straight from the go/v<version> git tag against this repo, and what it fetches is the tree at that tag, the static libraries under go/staticlib/ included. Each of them verifies from the release tag alone: the release attestation, signed by this repository's release.yml at the root tag, names every one of them, with the release inventory as its predicate. The same file sits in the module cache at $(go env GOMODCACHE)/github.com/!skunk!werkx/!hyper!cast/go@v<version>/staticlib/.

gh attestation verify go/staticlib/linux_amd64/libhypercast.a --repo SkunkWerkx/HyperCast \
  --source-ref refs/tags/v<version> --predicate-type https://github.com/SkunkWerkx/.github/predicates/release/v1

Each archive also carries build provenance from hyper-build-static.yml in SkunkWerkx/.github, at the preparation commit the tagged commit was staged from (stage-native-binaries.yml verifies that before committing a byte, and the staging commit's Release-Source-SHA and Release-CI-Run-ID trailers name the commit and the run), so that check needs --signer-repo beside --repo:

gh attestation verify go/staticlib/linux_amd64/libhypercast.a \
  --repo SkunkWerkx/HyperCast --signer-repo SkunkWerkx/.github

Get the signer wrong and gh reports a bare verifying with issuer "sigstore.dev", which reads like a bad signature but is only an identity mismatch. The root README's Provenance section has the whole picture: the inventory, which artifacts are signed from where, and the gates every byte passes before publication.

License

MIT

Documentation

Overview

The native backend: libhypercast linked into the binary. The core is a static library under staticlib/{goos}_{goarch}/, named on the cgo link line below, and every door is an ordinary C call to a symbol the linker resolved. Nothing is embedded, nothing is written to a temp directory, nothing is loaded at run time: a binary carries the core for its own platform, starts without touching the filesystem, runs from a read-only or `scratch` image, and has nothing that can fail to load.

That takes cgo, and so a C compiler wherever the module is built — gcc or clang on Linux, the Xcode command-line tools on macOS, a MinGW-w64 gcc (or llvm-mingw on arm64) on Windows. TinyGo compiling to WebAssembly links the same core from backend_tinygo.go — its cgo cannot parse this file's per-platform #cgo lines, hence `!tinygo` above. Anything else does not compile; unsupported.go says so by name. No other backend would reach a platform this one does not: a purego one extracting an embedded shared library, a cgo one loading that library instead of linking it, or a wasmtime one, since wasmtime-go itself needs cgo and ships engines only for these same platforms.

One archive serves both C libraries on Linux. cgo has no build constraint that tells glibc from musl, so there cannot be one per libc; the archive is the core built for the musl target, which asks the C library for nothing glibc and musl do not both have (memcpy, memset, bcmp). The suite runs against it on Debian and on Alpine.

Windows links the same MSVC archive C#'s Native AOT publish does: MinGW's linker reads MSVC's COFF objects, and what the archive asks of the C runtime — memcpy, memset, memcmp, and on x64 _fltused — resolves against msvcrt.dll through MinGW's own import library, so the link line names nothing else. The core imports nothing from Windows itself: parsing needs no system call.

iOS and Mac Catalyst link an archive of their own, since Go builds for both as GOOS=ios and every Mach-O object says which platform it was built for. GOOS=ios also satisfies the darwin constraint, so the macOS lines below say !ios. The three that share ios/arm64 are told apart by build tag: `maccatalyst`, which gomobile sets for that target; `iossimulator`, which nothing sets, so a simulator build passes it by hand; and neither, for a device. There is no archive for the simulator on amd64.

Android links an archive of its own too, for arm64 and amd64: the core built for the Android targets, which asks Bionic for what it asks musl for. GOOS=android satisfies the linux constraint, so the Linux lines below say !android. Building for it takes the NDK's clang as CC (CC=$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang GOOS=android GOARCH=arm64 CGO_ENABLED=1), as any cgo package on Android does.

cgo cannot call a function pointer directly — it needs a statically-typed C call site — hence one shim per ABI shape (plain, numeric, unix, version), each taking its door as a pointer. The shims also own the out-params: any Go pointer handed to a cgo call escapes to the heap, so instead of passing `&out`, `&fault` and `&format` across, the format and the day periods arrive BY VALUE, the C side declares them, the out-value and the fault span on its own stack, and hands the whole verdict back BY VALUE as one struct. No Go pointer crosses but the input bytes — a pointer value into memory that already exists, not an address taken of a local — and the success path allocates nothing (allocs_test.go holds that).

The archives are committed (staticlib/README.md): a Go module is whatever is in the tree at the resolved version, with no packing step to stage them in. The hypercast_local build tag links the archive .github/scripts/local-core.sh builds from the checkout instead, so the suite can run against the core as it stands without replacing a committed archive.

Package hypercast provides allocation-lean scalar casts — booleans, numerics, UUIDs, temporals — by calling the Rust core, linked into the binary as a static library through cgo (backend_static.go). It builds on Linux, macOS and Windows, on amd64 and arm64, with a C compiler present, and under TinyGo for WebAssembly, browser included (backend_tinygo.go); anything else is a compile error that says so (unsupported.go).

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 or an undefined enum value), never data.

Door names follow the native ABI's (cast_i32 is I32, cast_timestamp is Timestamp) so the polyglot surface reads the same across bindings, except where Go already owns the word: Exact and Span return the Decimal and Duration types their ABI names would collide with, and DateOnly, DateOnlyOrdered and TimeOfDay say what cast_date, cast_date_ordered and cast_time return. 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

Examples

Constants

View Source
const CurrencyMaxBytes = 16

CurrencyMaxBytes is the inline capacity of the ABI's currency symbol, in UTF-8 bytes.

View Source
const DesignatorMaxBytes = 16

DesignatorMaxBytes is the inline capacity of the ABI's day-period designators, in UTF-8 bytes.

View Source
const RawDayPeriodsSize = uint32(unsafe.Sizeof(RawDayPeriods{}))

RawDayPeriodsSize is the size word a RawDayPeriods carries: its own 40 bytes.

View Source
const RawNumFormatSize = uint32(unsafe.Sizeof(RawNumFormat{}))

RawNumFormatSize is the size word a RawNumFormat carries: its own 92 bytes.

View Source
const SymbolMaxBytes = 8

SymbolMaxBytes is the inline capacity of the ABI's separators, signs and percent symbol, in UTF-8 bytes: enough for every separator a real culture declares (one or two characters).

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, no currency symbol or other spelling declared.

Functions

func Available added in v0.3.0

func Available() bool

Available reports whether the core can be called. The core is linked into the binary (backend_static.go), so there is nothing to load and nothing that can fail: it is always true. It stays for code written against the load probe every binding carries.

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.

Example
package main

import (
	"fmt"

	hypercast "github.com/SkunkWerkx/HyperCast/go"
)

func main() {
	// The same text is January 7th or July 1st only because the caller said which.
	us, _ := hypercast.DateOnlyOrdered("1/7/2026", hypercast.MonthDayYear)
	gb, _ := hypercast.DateOnlyOrdered("1/7/2026", hypercast.DayMonthYear)
	fmt.Println(us.Month, us.Day)
	fmt.Println(gb.Month, gb.Day)
}
Output:
January 7
July 1

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, after the clock or ahead of it; absent, the time is midnight. No zone is read and none is invented — Timestamp stays the strict RFC 3339 instant door. The markers are the English AM/PM only; DateTimeWith reads a culture's own beside them. An undefined order is a caller bug and panics, never a verdict.

Example
package main

import (
	"fmt"
	"log"

	hypercast "github.com/SkunkWerkx/HyperCast/go"
)

func main() {
	// Zone-less text names a wall-clock date and time, not an instant — so that is what
	// comes back.
	civil, fault := hypercast.DateTime("1/7/2026 3:04 PM", hypercast.MonthDayYear)
	if fault != nil {
		log.Fatal(fault)
	}
	fmt.Println(civil.Date.Year, civil.Date.Month, civil.Date.Day, civil.TimeOfDay)
}
Output:
2026 January 7 15h4m0s

func DateTimeWith added in v0.9.0

func DateTimeWith[T Text](text T, format DateFormat) (CivilDateTime, *Fault)

DateTimeWith is DateTime under a declared DateFormat: the same grammar under its Order, with its AM and PM designators read beside the English markers ("1/7/2026 3:04 nachm.", "2026-01-07 오후 3:04"). A format declaring no designators reads exactly what DateTime reads. An undefined order or a designator the core would refuse is a caller bug and panics, never a verdict.

Example
package main

import (
	"fmt"
	"log"

	hypercast "github.com/SkunkWerkx/HyperCast/go"
)

func main() {
	// The culture's own day periods, declared beside the order; the English AM/PM are read
	// whatever is declared.
	german := hypercast.DateFormat{Order: hypercast.DayMonthYear, AM: "vorm.", PM: "nachm."}
	civil, fault := hypercast.DateTimeWith("1.7.2026 3:04 nachm.", german)
	if fault != nil {
		log.Fatal(fault)
	}
	korean := hypercast.DateFormat{Order: hypercast.YearMonthDay, AM: "오전", PM: "오후"}
	seoul, _ := hypercast.DateTimeWith("2026-07-01 오후 3:04", korean)
	fmt.Println(civil.Date.Day, civil.Date.Month, civil.TimeOfDay, civil == seoul)
}
Output:
1 July 15h4m0s true

func Exact added in v0.3.0

func Exact[T Text](text T, format NumFormat) (Decimal, *Fault)

Exact casts decimal text to a Decimal under the declared format — the same grammar and NumFormat as the real doors (declared separators and grouping, accounting parentheses, exponent, trailing percent, currency symbol) — but never rounded: a magnitude past 2^96 - 1 or a scale past 28 is OutOfRange, not approximated. Named Exact because the result type already owns the identifier Decimal, the way Span returns a Duration.

Example
package main

import (
	"fmt"
	"log"

	hypercast "github.com/SkunkWerkx/HyperCast/go"
)

func main() {
	dollars := hypercast.NumFormat{DecimalSep: ".", GroupSep: ",", Styles: hypercast.AllStyles, Currency: "$"}
	d, fault := hypercast.Exact("($1,234.50)", dollars)
	if fault != nil {
		log.Fatal(fault)
	}
	// Never rounded, and canonical: the trailing zero is trimmed.
	fmt.Println(d, d.Scale, d.Negative)
}
Output:
-1234.5 1 true

func ExactFromFloat64 added in v0.7.0

func ExactFromFloat64(value float64) (Decimal, *Fault)

ExactFromFloat64 reads a number the caller already holds — the float64 a workbook stores for a numeric cell — as the exact Decimal it names: the shortest decimal that rounds back to the float64, the digits a spreadsheet writes for it. So 0.1 is one tenth, not the binary fraction nearest it, and 0.1 + 0.2 is 0.30000000000000004 — the same digits strconv.FormatFloat(v, 'g', -1, 64) prints. The result is canonical, as Exact's is. NaN is Malformed; an infinity, a magnitude past 2^96 - 1 or more than 28 places is OutOfRange — the digits are never cut to fit. A typed door's Fault has no span: Offset and Length are 0.

Example
package main

import (
	"fmt"
	"log"

	hypercast "github.com/SkunkWerkx/HyperCast/go"
)

func main() {
	// The digits a spreadsheet writes for the double, not the binary fraction nearest it.
	// (Two variables, because Go adds the constants 0.1 + 0.2 exactly, to 0.3.)
	a, b := 0.1, 0.2
	d, fault := hypercast.ExactFromFloat64(a + b)
	if fault != nil {
		log.Fatal(fault)
	}
	fmt.Println(d)
}
Output:
0.30000000000000004

func ExcelDuration added in v0.7.0

func ExcelDuration(value float64) (Duration, *Fault)

ExcelDuration reads a number of days the caller already holds as a float64 — what an elapsed-time format ([h]:mm:ss) stores — as the protobuf pair Span returns: 1.5 is a day and twelve hours, and a negative span is negative, its size snapped as a serial's time. NaN or an infinity is Malformed; beyond ±10,000 years is OutOfRange.

func ExcelSerialFromFloat64 added in v0.7.0

func ExcelSerialFromFloat64(value float64, epoch ExcelEpoch) (CivilDateTime, *Fault)

ExcelSerialFromFloat64 reads an Excel date serial the caller already holds as a float64 under a caller-declared ExcelEpoch — the twin of ExcelSerial for a workbook reader that has the cell's number and no text. The result is the zone-less wall clock the cell holds, a CivilDateTime as DateTime returns, its fraction snapped: the time with the fewest fractional-second digits that the same float64 stores, so Excel's 23:59:59 is read on the second rather than the nanoseconds of float noise the double carries. The 1900 system's phantom serial 60, a serial below the system's first day and one past 9999-12-31 are OutOfRange; a negative, NaN or infinite serial is Malformed. An undefined epoch is a caller bug and panics, never a verdict.

Example
package main

import (
	"fmt"
	"log"

	hypercast "github.com/SkunkWerkx/HyperCast/go"
)

func main() {
	civil, fault := hypercast.ExcelSerialFromFloat64(45292.75, hypercast.Excel1900)
	if fault != nil {
		log.Fatal(fault)
	}
	fmt.Println(civil.Date.Year, civil.Date.Month, civil.Date.Day, civil.TimeOfDay)
}
Output:
2024 January 1 18h0m0s

func FormatVersion added in v0.7.0

func FormatVersion(packed uint32) string

FormatVersion renders a native library's packed version word — major<<16 | minor<<8 | patch, as a *_version() export returns it — as "major.minor.patch".

func LoadError added in v0.4.0

func LoadError() error

LoadError returns why the core could not be loaded. A linked core always can, so it is always nil. Kept for the reason Available is.

func NativeVersion added in v0.3.0

func NativeVersion() string

NativeVersion reports the linked core's own version as "major.minor.patch" — read from the core itself, not from this module — so a deployment can confirm which build of the archive went into the binary. It cannot fail.

Example
package main

import (
	"log"

	hypercast "github.com/SkunkWerkx/HyperCast/go"
)

func main() {
	// The version the linked core reports about itself, "major.minor.patch".
	log.Printf("hypercast core %s", hypercast.NativeVersion())
}

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.

Example
package main

import (
	"fmt"
	"log"

	hypercast "github.com/SkunkWerkx/HyperCast/go"
)

func main() {
	span, fault := hypercast.Span("PT1H30M15.5S")
	if fault != nil {
		log.Fatal(fault)
	}
	fmt.Println(span.Seconds, span.Nanos)

	// The checked converter: time.Duration tops out near 292 years, the core's window
	// does not.
	std, ok := span.AsDuration()
	fmt.Println(std, ok)
}
Output:
5415 500000000
1h30m15.5s true

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 ReasonFromCode added in v0.7.0

func ReasonFromCode(code uint32) (CastFailure, bool)

ReasonFromCode is the reason whose verdict code is code (1 empty, 2 malformed, 3 out of range), and false for any other value — 0, the code for success, included.

func (CastFailure) String

func (f CastFailure) String() string

String names the reason in lower case — "empty", "malformed", "out of range" — the form Fault.Error embeds.

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 DateFormat added in v0.9.0

type DateFormat struct {
	Order DateOrder
	// AM is the designator of the first half of the day; empty declares none beside AM.
	AM string
	// PM is the designator of the second half; empty declares none beside PM.
	PM string
}

DateFormat is the caller-declared notation of a zone-less civil date-time for DateTimeWith: the field order, and the words the culture writes for the two halves of a 12-hour clock ("vorm."/"nachm.", "오전"/"오후", "a. m."/"p. m."), the way NumFormat declares a numeric notation. Declared, never looked up — Go's standard library carries no culture data, so an application fills these from its own. The English AM/PM are read whatever is declared, ASCII case-insensitively; a declared designator is matched byte for byte, after the clock ("3:04 nachm.", optional space before) or ahead of it ("오후 3:04", optional space after). Empty designators declare nothing, which is exactly what DateTime reads.

A designator the core would refuse is a caller bug and panics, never a verdict: longer than DesignatorMaxBytes of UTF-8, not valid UTF-8, carrying an ASCII digit, or starting or ending in ASCII whitespace (the grammar around it reads that; an inner space, as in "a. m.", is fine). DateFormat.Raw reports the same check as an error.

func (DateFormat) Raw added in v0.9.0

func (f DateFormat) Raw() (raw RawDayPeriods, err error)

Raw validates the format's designators and lays them out as the core reads them — what DateTimeWith does with its format argument, as an error rather than the door's panic. The core's rules verbatim: each designator empty or at most DesignatorMaxBytes of valid UTF-8 with no ASCII digit and no ASCII whitespace at either end. The order is not checked here: it crosses beside the designators, and DateOrder.Valid reports it.

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.

func DateOrderFromCode added in v0.7.0

func DateOrderFromCode(code uint32) (DateOrder, bool)

DateOrderFromCode is the order whose ABI discriminant is code (1 year-month-day, 2 month-day-year, 3 day-month-year), and false for any other value.

func (DateOrder) Valid added in v0.7.0

func (o DateOrder) Valid() bool

Valid reports whether o is a defined order, the ones a door accepts.

type Decimal added in v0.3.0

type Decimal struct {
	// Lo is the low 64 bits of the magnitude.
	Lo uint64
	// Hi is the high 32 bits of the magnitude; the magnitude never exceeds 2^96 - 1.
	Hi uint32
	// Scale is the number of places the magnitude is shifted right, 0..=28.
	Scale uint8
	// Negative is true for a negative value; zero is never negative.
	Negative bool
}

Decimal is an exact decimal — a sign, a 96-bit unsigned magnitude, and a base-10 scale — the shape .NET's decimal stores natively and every arbitrary-precision decimal builds from directly. The value is (-1)^Negative × (Hi·2^64 + Lo) × 10^-Scale, Scale 0..=28. Never rounded: the Exact door reports text carrying more precision than that as OutOfRange rather than silently approximating. Canonical: exact trailing zeros in the fraction are trimmed so the scale is minimal ("1.10", "1.1" and "1.1000" are all magnitude 11, scale 1; zero is scale 0), and zero is never negative. Comparable, so two Decimals are == exactly when their values agree.

func (Decimal) Rat added in v0.3.0

func (d Decimal) Rat() *big.Rat

Rat is the exact value as a big.Rat — magnitude over 10^Scale, signed.

func (Decimal) String added in v0.3.0

func (d Decimal) String() string

String renders the canonical text form — "1234.5", "-0.025", "0" — the same string the conformance corpus pins as value.

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.

func ExcelEpochFromCode added in v0.7.0

func ExcelEpochFromCode(code uint32) (ExcelEpoch, bool)

ExcelEpochFromCode is the date system whose ABI discriminant is code (1 the 1900 system, 2 the 1904 system), and false for any other value.

func (ExcelEpoch) Valid added in v0.7.0

func (e ExcelEpoch) Valid() bool

Valid reports whether e is a defined date system, the ones a door accepts.

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.

Example
package main

import (
	"fmt"

	hypercast "github.com/SkunkWerkx/HyperCast/go"
)

func main() {
	yes, _ := hypercast.Bool("yes")
	off, _ := hypercast.Bool([]byte("OFF")) // string and []byte both cross zero-copy
	_, fault := hypercast.Bool("maybe")
	fmt.Println(yes, off, fault.Reason)
}
Output:
true false malformed

func Char added in v0.8.0

func Char[T Text](text T) (rune, *Fault)

Char casts one character: text that is exactly one UTF-8 scalar is that scalar verbatim, before any trimming (" " is a space, "6" is the digit six); otherwise, ASCII whitespace trimmed, exactly one declared code point — decimal 65, U+0041, 0x41, &H41, or an HTML numeric entity &#65; / &#x41; (the ; required), prefixes ASCII case-insensitive. A code point past U+10FFFF or a surrogate is OutOfRange; the rune returned is always a valid Unicode scalar.

Example
package main

import (
	"fmt"

	hypercast "github.com/SkunkWerkx/HyperCast/go"
)

func main() {
	// One character, verbatim or as a declared code point.
	a, _ := hypercast.Char("A")
	e, _ := hypercast.Char("U+00E9")
	smile, _ := hypercast.Char("&#x1F600;")
	_, fault := hypercast.Char("U+D800")
	fmt.Println(string(a), string(e), string(smile), fault.Reason)
}
Output:
A é 😀 out of range

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 OutOfRange 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.

Example
package main

import (
	"fmt"
	"log"

	hypercast "github.com/SkunkWerkx/HyperCast/go"
)

func main() {
	ts, fault := hypercast.ExcelSerial("45292.75", hypercast.Excel1900)
	if fault != nil {
		log.Fatal(fault)
	}
	fmt.Println(ts)
}
Output:
2024-01-01 18:00:00 +0000 UTC

func ExcelTime added in v0.7.0

func ExcelTime(value float64) (time.Duration, *Fault)

ExcelTime reads the fraction of an Excel serial the caller already holds as a float64 as a time.Duration since midnight, as TimeOfDay returns: 0.75 and 45292.75 are both 18h. Snapped as ExcelSerialFromFloat64 snaps, and a fraction that snaps to a whole day is midnight. A negative, NaN or infinite serial is Malformed; one past 9999-12-31 is OutOfRange.

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.

Example
package main

import (
	"fmt"
	"log"

	hypercast "github.com/SkunkWerkx/HyperCast/go"
)

func main() {
	// The caller declares the notation; nothing is guessed from the input.
	euros := hypercast.NumFormat{DecimalSep: ",", GroupSep: ".", Styles: hypercast.AllStyles, Currency: "€"}
	value, fault := hypercast.F64("€ 1.234,50", euros)
	if fault != nil {
		log.Fatal(fault)
	}
	fmt.Println(value)
}
Output:
1234.5
Example (Detect)
package main

import (
	"fmt"

	hypercast "github.com/SkunkWerkx/HyperCast/go"
)

func main() {
	// Detect resolves the separators' roles from the input's own structure — and refuses
	// to guess when the structure does not settle it.
	value, _ := hypercast.F64("1.234.567,89", hypercast.Detect)
	_, fault := hypercast.F64("1,000", hypercast.Detect)
	fmt.Println(value, fault.Reason)
}
Output:
1.23456789e+06 malformed

func FaultFromCode added in v0.7.0

func FaultFromCode(code, offset, length uint32) (*Fault, bool)

FaultFromCode is the fault a nonzero verdict code and its byte span name, and false when the code names no reason — a binding bug, not data.

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.

Example
package main

import (
	"fmt"
	"log"

	hypercast "github.com/SkunkWerkx/HyperCast/go"
)

func main() {
	// Accounting parentheses and grouping, under the declared invariant format.
	value, fault := hypercast.I32("(1,234)", hypercast.Invariant)
	if fault != nil {
		log.Fatal(fault)
	}
	fmt.Println(value)
}
Output:
-1234
Example (Fault)
package main

import (
	"fmt"

	hypercast "github.com/SkunkWerkx/HyperCast/go"
)

func main() {
	// A verdict, not an exception: the closed reason and the byte span that offended.
	_, fault := hypercast.I32("  12x4", hypercast.Invariant)
	fmt.Println(fault.Reason, fault.Offset, fault.Length)
	fmt.Println(fault)

	_, fault = hypercast.U8("256", hypercast.Invariant)
	fmt.Println(fault.Reason == hypercast.OutOfRange)
}
Output:
malformed 4 1
hypercast: malformed at byte 4..5
true

func I64

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

I64 casts integer text to int64. Rules as I8.

func Numeric added in v0.3.0

func Numeric[V Number, T Text](text T, format NumFormat) (V, *Fault)

Numeric casts numeric text to V under the declared format by dispatching to V's own door — Numeric[int32] is I32, Numeric[Decimal] is Exact — with T inferred from the argument: hypercast.Numeric[int32]("42", format). The rules and verdicts are the concrete door's, unchanged. An unsupported V is impossible by construction: Number lists exactly the types that have a door, so the switch below is exhaustive and the compiler rejects anything else at the call site.

Example
package main

import (
	"fmt"

	hypercast "github.com/SkunkWerkx/HyperCast/go"
)

func main() {
	// One door generic over the target: Numeric[int32] is I32, Numeric[Decimal] is Exact.
	n, _ := hypercast.Numeric[int32]("42", hypercast.Invariant)
	f, _ := hypercast.Numeric[float64]("50%", hypercast.Invariant)
	d, _ := hypercast.Numeric[hypercast.Decimal]("1.10", hypercast.Invariant)
	fmt.Println(n, f, d)
}
Output:
42 0.5 1.1

func Scalar added in v0.8.0

func Scalar[V ScalarTarget, T Text](text T, format NumFormat) (V, *Fault)

Scalar casts text to V by dispatching to V's own door, the way Numeric does for the numeric targets: Scalar[bool] is Bool, Scalar[uuid.UUID] is Uuid, Scalar[time.Time] is Timestamp (RFC 3339, zone mandatory, normalized to UTC), Scalar[Date] is DateOnly (strict yyyy-MM-dd), Scalar[Duration] is Span, and every Number is Numeric. format is read only by the numeric doors. The rules and verdicts are the concrete door's, unchanged, and an unsupported V is a compile error: ScalarTarget lists exactly the types that have a door.

Example
package main

import (
	"fmt"
	"time"

	hypercast "github.com/SkunkWerkx/HyperCast/go"
)

func main() {
	// One door generic over every target with a door of its own: Scalar[bool] is Bool,
	// Scalar[time.Time] is Timestamp, Scalar[int32] is I32.
	b, _ := hypercast.Scalar[bool]("yes", hypercast.Invariant)
	at, _ := hypercast.Scalar[time.Time]("2026-01-02T15:04:05+05:00", hypercast.Invariant)
	n, _ := hypercast.Scalar[int32]("1,234", hypercast.Invariant)
	fmt.Println(b, at.Format(time.RFC3339), n)
}
Output:
true 2026-01-02T10:04:05Z 1234

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.

Example
package main

import (
	"fmt"
	"log"

	hypercast "github.com/SkunkWerkx/HyperCast/go"
)

func main() {
	sinceMidnight, fault := hypercast.TimeOfDay("15:04:05.5")
	if fault != nil {
		log.Fatal(fault)
	}
	fmt.Println(sinceMidnight)
}
Output:
15h4m5.5s

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.

Example
package main

import (
	"fmt"
	"log"

	hypercast "github.com/SkunkWerkx/HyperCast/go"
)

func main() {
	// RFC 3339, zone mandatory, normalized to UTC at full nanosecond fidelity.
	ts, fault := hypercast.Timestamp("2026-01-02T15:04:05.123456789+05:00")
	if fault != nil {
		log.Fatal(fault)
	}
	fmt.Println(ts)
}
Output:
2026-01-02 10:04:05.123456789 +0000 UTC

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.

Example
package main

import (
	"fmt"

	hypercast "github.com/SkunkWerkx/HyperCast/go"
)

func main() {
	// The unit is declared, never inferred from the magnitude.
	fromSeconds, _ := hypercast.Unix("1700000000", hypercast.Seconds)
	fromMillis, _ := hypercast.Unix("1700000000000", hypercast.Milliseconds)
	fmt.Println(fromSeconds, fromSeconds.Equal(fromMillis))
}
Output:
2023-11-14 22:13:20 +0000 UTC true

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).

Example
package main

import (
	"fmt"
	"log"

	hypercast "github.com/SkunkWerkx/HyperCast/go"
)

func main() {
	id, fault := hypercast.Uuid("urn:uuid:01020304-0506-0708-090a-0b0c0d0e0f10")
	if fault != nil {
		log.Fatal(fault)
	}
	fmt.Println(id)
}
Output:
01020304-0506-0708-090a-0b0c0d0e0f10

func (*Fault) Error

func (f *Fault) Error() string

Error renders the verdict as "hypercast: {reason} at byte {start}..{end}", the span as a half-open byte range into the input. A nil *Fault — a door's success, printed — renders as "<nil>", what fmt prints for one under stock Go after recovering the nil dereference. TinyGo's wasm targets cannot recover a panic, so without this the same print trapped there.

type NumFormat

type NumFormat struct {
	// DecimalSep is the decimal separator ('.' invariant, ',' in eurozone text): 1 to
	// SymbolMaxBytes bytes of UTF-8 with no ASCII digit. Required.
	DecimalSep string
	// GroupSep is the digit-group separator (',' invariant, '.', ' ' or ", " elsewhere),
	// likewise; must differ from DecimalSep. Required.
	GroupSep string
	Styles   NumStyles
	// Currency is the symbol the CurrencySymbol style accepts ("$", "€", "kr.", "CHF") —
	// matched whole at the edges of the numeric body, never inside the digit scan, so a
	// symbol containing a separator character is fine. Empty declares none.
	Currency string
	// CurrencyDecimalSep is the decimal separator of a currency amount, read in place of
	// DecimalSep when CurrencySymbol is set, Currency is declared and the symbol is found
	// in the text — the way a culture's currency pattern may group and point differently
	// from its number pattern. Empty keeps DecimalSep. Ignored under SeparatorDetect, whose
	// detection governs the whole token.
	CurrencyDecimalSep string
	// CurrencyGroupSep is the group separator of a currency amount, likewise; empty keeps
	// GroupSep.
	CurrencyGroupSep string
	// MinusSign is a further spelling of the minus sign ("−", U+2212, in Estonian and
	// Basque text), read wherever ASCII '-' is: ahead of the digits, before or after a
	// leading currency symbol, and after the digits under TrailingSign. ASCII '-' is always
	// read; empty declares no other.
	MinusSign string
	// PlusSign is a further spelling of the plus sign, likewise beside ASCII '+'.
	PlusSign string
	// Percent is a further spelling of the percent symbol ("٪" in Arabic-script text, "%"
	// fullwidth), read as a trailing '%' is while Percent is set — the real and decimal
	// doors; the integer doors never read a percent. ASCII '%' is always read.
	Percent string
	// DigitZero declares native digits beside ASCII 0–9: the ten code points from this zero
	// up ('٠', U+0660, declares the Arabic-Indic digits; '0', U+FF10, the fullwidth ones),
	// read wherever a digit is — integer part, fraction, exponent; a radix-prefixed (0x/0b)
	// body stays ASCII. 0 declares none. An ASCII zero is a caller bug, as is a code point
	// that starts no Unicode digit block.
	DigitZero rune
}

NumFormat is the caller-declared numeric notation for the integer, real and decimal doors. The core carries no culture data, and neither does Go's standard library, so there is nothing here to bridge from — a call site parsing culture-sensitive text declares its format out loud (Invariant, or a literal filled from whatever culture data the application holds); there is no default, the same stance every binding in this repo takes. The two number separators are the only required fields; every other field declares a further spelling the core reads beside the ASCII one, so a format that declares nothing else reads exactly what Invariant reads, and nothing that parsed before a symbol was declared stops parsing once it is.

A format the core would refuse is a caller bug and panics, never a verdict: a number separator that is empty or longer than SymbolMaxBytes, equal number separators (or equal separators in force for a currency amount), any symbol that is not valid UTF-8 or carries an ASCII digit, a sign or percent symbol carrying ASCII whitespace, a Currency longer than CurrencyMaxBytes or carrying ASCII whitespace, or a DigitZero that starts no Unicode digit block. NumFormat.Raw reports the same check as an error.

Example
package main

import (
	"fmt"

	hypercast "github.com/SkunkWerkx/HyperCast/go"
)

func main() {
	// Everything past the separators declares a further spelling the core reads beside the
	// ASCII one — the minus sign U+2212, a trailing sign, native digits — so "-5" keeps
	// parsing under a format that also reads "−5", "5−" and "٥".
	estonian := hypercast.NumFormat{
		DecimalSep: ",", GroupSep: " ", Styles: hypercast.AllStyles | hypercast.TrailingSign,
		Currency: "€", MinusSign: "−",
	}
	a, _ := hypercast.F64("−1 234,5 €", estonian)
	b, _ := hypercast.F64("1 234,5 €−", estonian)
	c, _ := hypercast.F64("-1234.5", hypercast.Invariant)
	arabic := hypercast.NumFormat{DecimalSep: "٫", GroupSep: "٬", Styles: hypercast.AllStyles, DigitZero: '٠', Percent: "٪"}
	d, _ := hypercast.F64("١٢٫٥٪", arabic)
	fmt.Println(a, b, c, d)
}
Output:
-1234.5 -1234.5 -1234.5 0.125

func (NumFormat) Raw added in v0.7.0

func (f NumFormat) Raw() (raw RawNumFormat, err error)

Raw validates the format and lays it out as the core reads it — what every numeric door does with its format argument, as an error rather than the doors' panic. The core's rules verbatim: each number separator one to SymbolMaxBytes UTF-8 bytes with no ASCII digit, the two different; each currency separator the same or empty, and the pair in force for a currency amount different; each sign and the percent symbol the same or empty, with no ASCII whitespace; the currency symbol at most CurrencyMaxBytes, no digit, no whitespace; the digit zero 0, or a code point outside ASCII that starts a run of ten digits sharing its UTF-8 prefix (every Unicode digit block does; one whose value modulo 64 exceeds 54 does not).

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

	// CurrencySymbol permits the format's declared Currency once, at either edge of the
	// numeric body: leading, before or after a sign ("$5", "-$5", "$ -5"), or trailing
	// ("5 €", "1.234,50 kr."), with optional ASCII whitespace between symbol and digits;
	// accounting parentheses wrap symbol and digits together ("($5)"). With no Currency
	// declared the flag matches nothing and changes nothing. A Currency declared while
	// this flag is off is Malformed at the symbol. Every integer, real and decimal door.
	CurrencySymbol NumStyles = 1 << 6

	// TrailingSign permits the sign after the digits ("5-", "5 -", "5+", "5 €-", "5- €",
	// "2.5%-"), the way a culture whose negative pattern is "n-" writes it: ASCII '-'/'+'
	// or the declared MinusSign/PlusSign, once, with optional ASCII whitespace before it,
	// and never beside a leading sign or inside accounting parentheses. Not part of
	// AllStyles: a caller sets it from the culture's own negative pattern.
	TrailingSign NumStyles = 1 << 7

	// AllStyles is every lenience on but TrailingSign, which is declared from a culture's
	// negative pattern rather than assumed.
	AllStyles = Grouping | Parentheses | Exponent | RadixPrefixes | Percent | CurrencySymbol
)

type Number added in v0.3.0

type Number interface {
	int8 | int16 | int32 | int64 | uint8 | uint16 | uint32 | uint64 | float32 | float64 | Decimal
}

Number is the closed set of targets the numeric doors cast to — exactly the eleven types I8 through U64, F32, F64 and Exact return. It exists for Numeric: a caller that is itself generic over the target names the type once instead of writing the door switch.

type RawCivil added in v0.7.0

type RawCivil struct {
	Year  uint16
	Month uint8
	Day   uint8

	Nanos uint64
	// contains filtered or unexported fields
}

RawCivil is a zone-less wall clock as the core writes it: a date and the nanoseconds since its midnight. 16 bytes.

func (RawCivil) CivilDateTime added in v0.7.0

func (r RawCivil) CivilDateTime() CivilDateTime

CivilDateTime is the wall clock: the DateTime doors' value.

type RawDate added in v0.7.0

type RawDate struct {
	Year  uint16
	Month uint8
	Day   uint8
}

RawDate is a calendar date as the core writes it. 4 bytes.

func (RawDate) Date added in v0.7.0

func (r RawDate) Date() Date

Date is the date: the DateOnly doors' value.

type RawDayPeriods added in v0.9.0

type RawDayPeriods struct {
	Size uint32
	AM   RawSymbol16
	PM   RawSymbol16
	// contains filtered or unexported fields
}

RawDayPeriods is a DateFormat's designators as they cross the ABI beside the declared DateOrder — 40 bytes, 4-aligned: a size word (RawDayPeriodsSize), the AM designator inline at byte 4 and the PM one at 21, each its bytes followed by its length, and two bytes of padding. A zero length declares nothing beside the English marker; where the core takes a pointer to one, null declares nothing at all. DateFormat.Raw builds one.

type RawNumFormat added in v0.7.0

type RawNumFormat struct {
	Size               uint32
	Flags              uint32
	DigitZero          uint32
	DecimalSep         RawSymbol8
	GroupSep           RawSymbol8
	CurrencyDecimalSep RawSymbol8
	CurrencyGroupSep   RawSymbol8
	MinusSign          RawSymbol8
	PlusSign           RawSymbol8
	Percent            RawSymbol8
	Currency           RawSymbol16
}

RawNumFormat is NumFormat as it crosses the ABI — 92 bytes, 4-aligned, the core reading the caller's struct in place: a size word (RawNumFormatSize, so the layout can grow without another coordinated change), the flags, the native digits' zero code point (0 for none), then every symbol inline, its bytes followed by its length. Byte offsets: Flags 4, DigitZero 8, DecimalSep 12, GroupSep 21, CurrencyDecimalSep 30, CurrencyGroupSep 39, MinusSign 48, PlusSign 57, Percent 66, Currency 75. NumFormat.Raw builds one.

type RawSymbol8 added in v0.9.0

type RawSymbol8 struct {
	Bytes [SymbolMaxBytes]byte
	Len   uint8
}

RawSymbol8 is a separator, sign or percent symbol as it crosses the ABI: its UTF-8 bytes held inline (zero-padded) and their count, 9 bytes with no alignment. A zero length declares none.

func (RawSymbol8) String added in v0.9.0

func (s RawSymbol8) String() string

String is the symbol's text — empty when none is declared.

type RawSymbol16 added in v0.9.0

type RawSymbol16 struct {
	Bytes [CurrencyMaxBytes]byte
	Len   uint8
}

RawSymbol16 is a currency symbol or a day-period designator as it crosses the ABI, the same shape at 16 bytes of capacity: 17 bytes with no alignment.

func (RawSymbol16) String added in v0.9.0

func (s RawSymbol16) String() string

String is the symbol's text — empty when none is declared.

type RawTimestamp added in v0.7.0

type RawTimestamp struct {
	Seconds int64
	Nanos   int32
	// contains filtered or unexported fields
}

RawTimestamp is an instant as the core writes it — protobuf's Timestamp: whole seconds from the Unix epoch and the nanoseconds after them. 16 bytes.

func (RawTimestamp) Time added in v0.7.0

func (r RawTimestamp) Time() time.Time

Time is the instant at UTC, at full nanosecond fidelity: the Timestamp, Unix and ExcelSerial doors' value.

type ScalarTarget added in v0.8.0

type ScalarTarget interface {
	Number | bool | uuid.UUID | time.Time | Date | Duration
}

ScalarTarget is the closed set of types Scalar casts to: every Number, plus bool, uuid.UUID, time.Time, Date and Duration — each the one type a single door returns without a declared argument. Three are left out on purpose:

  • rune: it is int32, so Scalar[int32] is I32's integer door; call Char for a character.
  • time.Duration: TimeOfDay returns one, but a Go reader takes time.Duration for a span, and Span returns Duration because time.Duration cannot hold the core's window.
  • CivilDateTime: zone-less date-times need a declared DateOrder; call DateTime.

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.

func UnixPrecisionFromCode added in v0.7.0

func UnixPrecisionFromCode(code uint32) (UnixPrecision, bool)

UnixPrecisionFromCode is the precision whose ABI discriminant is code (1 seconds … 4 nanoseconds), and false for any other value.

func (UnixPrecision) Valid added in v0.7.0

func (p UnixPrecision) Valid() bool

Valid reports whether p is a defined precision, the ones a door accepts.

Directories

Path Synopsis
internal
tinygosmoke command
Command tinygosmoke is the check CI runs in headless Chrome: this module compiled by TinyGo for the browser (`tinygo build -target=wasm`), with the core linked in from staticlib/wasm, loaded by index.html beside it through TinyGo's own wasm_exec.js.
Command tinygosmoke is the check CI runs in headless Chrome: this module compiled by TinyGo for the browser (`tinygo build -target=wasm`), with the core linked in from staticlib/wasm, loaded by index.html beside it through TinyGo's own wasm_exec.js.

Jump to

Keyboard shortcuts

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