Sky stdlib — Canonical correctness reference

Pairs with docs/rust-rewrite/ (the primary compiler reference; the Haskell-era docs/architecture/sky-compiler-architecture.md is historical context only). The compiler docs explain HOW Sky source becomes Go; this doc explains WHAT the stdlib surfaces guarantee.

This reference is grounded in the v0.17 HEAD sources at sky-stdlib/, the Rust kernel registry, and the Go runtime in runtime-go/rt/. Every claim either cites a file:line location, flags itself UNVERIFIED, or notes the regression spec that proves it.


1. Overview

1.1 What the stdlib is

Sky's stdlib is Layer-3: every kernel module is surfaced as Sky source under sky-stdlib/{Sky/Core,Std,Sky/Http}/*.sky (13,091 lines as of HEAD). Each binding is one of:

  1. Pure Sky — recursive / case-of / accumulator implementation. List.foldl, Maybe.map, Result.combine, every Std.Ui combinator.
  2. Ffi.kernel "Name" alias — Sky-source typed declaration whose call sites the compiler routes to the Haskell-side kernel dispatch, which then calls a Go runtime function in runtime-go/rt/. Zero runtime cost over a direct call; sky doc surfaces the entry.

The stdlib is the public contract. Anything not surfaced here is private compiler/runtime machinery and may change without notice.

1.2 Backends

BackendView renders asEffect runtime
Sky.LiveHTML over HTTP + SSE diffsGoroutine pool + session store
Sky.TuiANSI cells (rivo/uniseg)Goroutine pool + raw TTY
Sky.WebviewWKWebView (macOS v0.1)Same as Sky.Live, no HTTP
Sky.Clistdio / Std.LogGoroutine pool
Sky.Http.ServerPlain HTTP responsesGoroutine pool

All five share the same Element msg / Html msg / Cmd msg / Sub msg ADTs, so a view function paints identically across them (modulo <style>-driven primitives, which Sky.Tui silently ignores).

1.3 Compiler version

Compiler version this doc tracks: v0.16.6 release candidate (per CLAUDE.md current state) with the v0.17 typed-codegen close in progress on feat/v0.17-fully-typed-codegen. Every law and emission rule below holds against that compiler. Where v0.17 changes a surface, the change is called out inline.

1.4 Effect tiers (single rule)

Every observable side effect returns Task Error a.

flowchart LR
    Pure[Pure: bare a] --> Fallible[Fallible-pure: Result e a / Maybe a]
    Fallible --> Effects[Effects: Task Error a]
    Effects --> Diverging[Diverging: Int -> a]
TierExamples
PureString.length, List.map, Crypto.sha256, Time.timeString
Fallible-pureString.toInt, JSON decoders, Auth.hashPassword
EffectsFile.*, Http.*, Db.*, Time.now, Crypto.randomBytes
DivergingSystem.exit : Int -> a

System.getenvOr key default : String stays bare — the default plugs the failure case at the call site, no Task wrap needed.


2. Sky.Core algebraic primitives

2.1 Sky.Core.Maybe

File: sky-stdlib/Sky/Core/Maybe.sky (250 lines).

Type:

type Maybe a = Just a | Nothing

Surface (signatures verified at lines indicated):

FunctionSignatureFile:line
withDefaulta -> Maybe a -> aMaybe.sky:42
map(a -> b) -> Maybe a -> Maybe bMaybe.sky:52
andThen(a -> Maybe b) -> Maybe a -> Maybe bMaybe.sky:62
map2..5(a -> b -> ... -> z) -> Maybe a -> ... -> Maybe zMaybe.sky
andMapMaybe a -> Maybe (a -> b) -> Maybe bapplicative <*>
combineList (Maybe a) -> Maybe (List a)Maybe.sky:193
isJust/isNothingMaybe a -> BoolMaybe.sky
toResulte -> Maybe a -> Result e aMaybe.sky + rt.Maybe_toResult

Mathematical claims (Functor / Applicative / Monad):

Partial functions: NONE. Every arm of every public function is total. withDefault plugs Nothing; map/andThen propagate.

Invariants:

Verification: by inspection of the case-arm structure. No dedicated property-test spec for the algebraic laws (UNVERIFIED by test). Maybe is small enough that the law-by-inspection bar is met.

Known gaps: None.


2.2 Sky.Core.Result

File: sky-stdlib/Sky/Core/Result.sky (228 lines).

Type:

type Result e a = Ok a | Err e

Surface:

FunctionSignatureFile:line
withDefaulta -> Result e a -> aResult.sky:27
map(a -> b) -> Result e a -> Result e bResult.sky:37
andThen(a -> Result e b) -> Result e a -> Result e bResult.sky:47
mapError(e -> e2) -> Result e a -> Result e2 aResult.sky
map2..5(a -> b -> ... -> z) -> Result e a -> ... -> Result e zResult.sky
andMapResult e a -> Result e (a -> b) -> Result e bapplicative <*>
combineList (Result e a) -> Result e (List a)Result.sky:199
toMaybeResult e a -> Maybe aResult.sky + rt.Result_toMaybe
fromMaybee -> Maybe a -> Result e aResult.sky + rt.Result_fromMaybe

Mathematical claims:

Result e is a Bifunctor (functor in both e via mapError and a via map) and a Monad in a (Err e short-circuits).

Partial functions: NONE.

Invariants:

Verification: by inspection. The CLAUDE.md non-regression rule "no Result String a in public surfaces" is enforced by the compiler test suite (cargo test, error-shape gates).

Known gaps: None.


2.3 Sky.Core.List

File: sky-stdlib/Sky/Core/List.sky (543 lines, the largest core algebraic module).

Type: builtin List a (compiler ADT, not Sky-source declared — the Sky type is [a] syntactic sugar over a cons cell).

Surface (key entries):

FunctionSignatureTCO classFile:line
map(a -> b) -> List a -> List bCPSList.sky:52
foldl(a -> b -> b) -> b -> List a -> bauto-TCOList.sky:138
foldr(a -> b -> b) -> b -> List a -> bCPSList.sky:160
filter(a -> Bool) -> List a -> List aCPSList.sky
concatList (List a) -> List aCPSList.sky
concatMap(a -> List b) -> List a -> List baccumulatorList.sky
take/dropInt -> List a -> List aCPS / TCOList.sky
appendList a -> List a -> List aCPSList.sky
lengthList a -> IntCPSList.sky
rangeInt -> Int -> List IntCPSList.sky
reverseList a -> List aauto-TCOvia reverseHelp
member/any/all/find(a -> Bool) -> List a -> Bool/Maybe aauto-TCOList.sky
zipList a -> List b -> List (a, b)CPSList.sky
indexedMap(Int -> a -> b) -> List a -> List bCPSList.sky

Mathematical claims:

Partial functions:

Invariants:

Verification: per-op spec under test/Sky/Build/CpsStackConstantBound/ (each verifies the rewritten body emits the auto-TCO marker — i.e. that the rewrite landed, not the law itself). Law-by-inspection for the algebraic content.

Known gaps:


2.4 Sky.Core.Dict and Sky.Core.Set

Files: Dict.sky (127 lines), Set.sky (92 lines). Both are Ffi.kernel aliases — implementations live in Go runtime (runtime-go/rt/rt.go — there are no dict.go / set.go files; the Dict_* / Set_* kernels live in rt.go and stdlib_extra.go).

Types:

type Dict k v  -- opaque; runtime is sync.Map for typed-key dicts,
               -- plain map[K]V for monomorphic specialisations
type Set a     -- opaque; sorted slice or hashset depending on a

Surface highlights:

FunctionSignature
Dict.emptyDict k v
Dict.insertk -> v -> Dict k v -> Dict k v
Dict.getk -> Dict k v -> Maybe v
Dict.removek -> Dict k v -> Dict k v
Dict.memberk -> Dict k v -> Bool
Dict.keysDict k v -> List k
Dict.toListDict k v -> List (k, v)
Dict.unionDict k v -> Dict k v -> Dict k v
Set.fromListList a -> Set a
Set.union/diff/intersectSet a -> Set a -> Set a

Mathematical claims:

Partial functions: NONE.

Invariants:

Verification: tests/ under stdlib exercises basic membership + union; examples/00-standard-libs exercises both modules in the 120-assertion stdlib smoke test.

Known gaps:


2.5 Sky.Core.String

File: String.sky (218 lines). 38 entries. All Ffi.kernel aliases backed by Go runtime functions that respect Unicode where the documentation says they do.

Key signatures:

length         : String -> Int                    -- rune count, not byte count
reverse        : String -> String                 -- rune-aware
toInt          : String -> Maybe Int
fromInt        : Int -> String
toFloat        : String -> Maybe Float
fromFloat      : Float -> String
toUpper        : String -> String                 -- Unicode-aware
toLower        : String -> String
trim/trimStart/trimEnd : String -> String          -- trims Unicode whitespace
contains/startsWith/endsWith : String -> String -> Bool  -- needle-first
containsIn/startsWithIn/endsWithIn : String -> String -> Bool  -- haystack-first (v0.15.47+)
slice          : Int -> Int -> String -> String   -- rune indices, Elm semantics
dropLeft/dropRight : Int -> String -> String       -- rune-based (v0.16.31)
casefold/equalFold : ...                            -- Unicode case-folded comparison
isEmail/isUrl   : String -> Bool                    -- format checks
words/lines    : String -> List String

Mathematical claims:

Partial functions: NONE. toInt / toFloat return Maybe. slice clamps out-of-bounds indices.

Invariants:

Verification: stdlib smoke test (examples/00) hits ~30 of 38 entries. The compiler test suite's UI-layout specs don't cover String, but the tests/Sky.Core.String* suite covers padding, trim, and contains.

Known gaps:


2.6 Sky.Core.Math (36 entries)

File: Math.sky (206 lines).

Surface: abs, min, max, sqrt, pow, cbrt, hypot, exp, exp2, log, log2, log10, floor, ceil, round, trunc, sin, cos, tan, asin, acos, atan, atan2, sinh, cosh, tanh, asinh, acosh, atanh, mod, remainder, plus constants pi, e, phi, sqrt2, inf, nan.

Mathematical claims:

Verification: examples/00-standard-libs smoke. The parity-with-Go is by-inspection of the runtime binding (one-line delegates).

Known gaps:


2.7 Sky.Core.Crypto

File: Crypto.sky (158 lines).

Surface:

FunctionSignatureTier
sha256/sha512/sha1/md5String -> Stringpure
hmacSha256/hmacSha512String -> String -> Stringpure
rsaSha256Sign/VerifyString -> String -> ...Result Error _
constantTimeEqualString -> String -> Boolpure
xchachaSeal/xchachaSealWithSecret -> String (-> String) -> Task Error Stringeffect (random nonce)
xchachaOpen/xchachaOpenWithSecret -> String (-> String) -> Result Error Stringfallible-pure
aesGcmEncrypt/chacha20EncryptSecret -> String -> Task Error Stringeffect (random nonce; a Task since v0.27.0)
aesGcmDecrypt/chacha20DecryptSecret -> String -> Result Error Stringfallible-pure
chacha20Poly1305Seal/Open, xchacha20Poly1305Seal/OpenSecret -> String -> String -> String -> Result Error Stringfallible-pure (caller nonce; deterministic)
aesKeyFromPassword/chachaKeyFromPasswordSecret -> String -> Secretpure (PBKDF2)
randomBytesInt -> Task Error Stringeffect
randomTokenInt -> Task Error Stringeffect

Security claims:

Verification: runtime-go/rt/crypto_aead_test.go covers the AEAD round-trips, tamper and wrong-key refusal, and the Task shape; crypto_sign_kx_test.go the XChaCha20-Poly1305 draft vector.

Known gaps:

2.7a Std.Crypto.Sign / Kx / Kdf / Noise / Cpace, Std.Qr (v0.27.0)

Laws and invariants, each with its test (runtime-go/rt/crypto_sign_kx_test.go, noise_test.go, cpace_test.go, qr_test.go; conformance suites CryptoPrimitives, Noise, Cpace, Qr; corpus Family S surfaces sign, kx, kdf, noise, cpace, qr):


2.8 Sky.Core.Jwt

File: Jwt.sky (297 lines).

Surface:

type Algorithm = HS256 String | RS256 String   -- secret / privateKey
type Claims                                     -- opaque builder

encode : Algorithm -> Claims -> Result Error String
decode : Algorithm -> Int -> String -> Result Error Claims
  -- second arg is `now` in unix seconds; checks exp + nbf

claims : Claims  -- empty
issuer / subject / audience / expiresAt / notBefore / issuedAt
 / jwtId / withClaim : ... -> Claims -> Claims  -- builders

Security claims:

Verification: runtime-go/rt/jwt_test.go covers round-trip, exp/nbf rejection, signature mismatch, alg-substitution attempt. Issue #555 fix (typed-Dict claims round-trip via gob) lives in Auth_signToken runtime — claims are no longer dropped at signing.

Known gaps:


2.9 Sky.Core.Task (concurrency + effects)

File: Task.sky (305 lines).

Type: Task e a — opaque; runtime is a func() (Result[e,a], error) thunk wrapped in a typed dispatch (since v0.15.46, all Task callbacks are typed SkyTask[E, T] so the kernel knows the exact runtime shape).

Surface:

FunctionSignature
succeeda -> Task e a
faile -> Task e a
map(a -> b) -> Task e a -> Task e b
andThen(a -> Task e b) -> Task e a -> Task e b
mapError(e -> e2) -> Task e a -> Task e2 a
onError(e -> Task e2 a) -> Task e a -> Task e2 a
fromResultResult e a -> Task e a
andThenResult(a -> Result e b) -> Task e a -> Task e b
sequenceList (Task e a) -> Task e (List a)
parallelList (Task e a) -> Task e (List a)
parallelNInt -> List (Task e a) -> Task e (List a)
performTask e a -> (Result e a -> msg) -> Cmd msg
runTask e a -> Result e a (synchronous main-only)
lazy(() -> a) -> Task e a
retryWithRetryPolicy e -> Task e a -> Task e a
loop(state -> Task e (Step state a)) -> state -> Task e a
foreverTask e a -> Task e b
spawnTask e a -> Task e ()

Mathematical claims:

Effect-boundary law: every Task at the top of main MUST be forced — either by explicit Task.run (a one-shot CLI) or by the runtime entry point (App.run, Server.listen). A let _ = TaskExpr inside Sky source is auto-forced by the compiler via rt.AnyTaskRun. This is structural — the compiler emits the wrap; the user does not opt in.

Synchronous panic gate (v0.15.43): every emitted func main() starts with defer rt.LogPanicAndExit(). A panic in synchronous Sky (Sky.Cli / batch / non-server main) is recovered, classified (DivisionByZero / TypeMismatch / CoerceFailure / etc.), logged with a 4-byte errId, and the process exits 1. Server handlers have a per-request defer/recover. The Cmd.perform goroutine wraps rt.SafeGo.

Verification: runtime tests in runtime-go/rt/task_test.go, retry_test.go, task_loop_test.go (2,000,000 steps under a 1 MB stack cap, flat heap, plus a child-process control proving a nested force still overflows the same cap), task_trampoline_test.go (2,000,000 steps of andThen, onError and left-nested recursion in child processes at a 1 MB cap; constant heap for Task.forever and a recursive service loop; free TaskCoerceT; every force site; classified fallbacks), task_spawn_log_test.go; Sky suite tests/conformance/tests/TaskLoopConformanceTest.sky, and rust/crates/sky/tests/task_trampoline_flow.rs (a compiled Sky program). Examples 07-todo-cli and 18-job-queue exercise the two-level error pattern (correlation ID + structured log + user message).

Known gaps:


2.10 Other Sky.Core modules (terse)

ModuleLinesPurposeCorrectness verdict
Basics49identity, always, not, clamp, modBy, comparePure, by-inspection
Bytes112empty/length/fromString/toString/fromHex/toHex/append/sliceRound-trips by-inspection
Char57isAlpha/isDigit/isLower/isUpper/toUpper/toLower/toCode (v0.16.7 #419)Unicode-aware
Encoding47base64/urlEncode/hexEncode + inversesRound-trip pairs
Path27base/dir/ext/isAbsoluteOS-aware via filepath
Process418run; streaming spawn with an output ring, PTY, signals (v0.27.0)Effect; see Task tier and §2.10a
Regex42match/find/findAll/replace/splitRE2 backed (Go) — no catastrophic backtracking
Random144int/float/range/choice/shuffle/weighted; seed variantsEntropy: crypto/rand for Task tier; splitmix64 for seeded tier
Uuid38v4 (random) / v7 (timestamp-ordered) / parseRFC 4122 compliant
Http142get/post/request + parseQuery; HttpResponse typed recordEffect; see Task tier; TLS via Go net/http
WebSocket297client + server bidirectional socketsBuilt on nhooyr.io/websocket; production gate refuses empty originPatterns when ENV=production
Time76now/sleep/every/unixMillis/format*/timeStringUTC by default; IANA zones via Std.Time
ToString54fromInt/fromFloat/fromBool/fromTimeNaming-consistency aliases; zero runtime cost
Pure143uniform () -> Task Error a companion surface (v0.15.50+)Tail-call aliases; HM-portable
Tuple74pair/first/second/mapFirst/mapSecond/mapBoth (v0.27.0)Pure Sky, total; first == fst, second == snd
Json.Encode / Json.Decode—builders + decoders over one shared Value type; Decode.value (raw tree, json.Number keeps exact number text), Encode.raw (validated json.RawMessage)Decode is total (Result Error a). encode is total except for a NaN/infinite Float, which is the classified JsonEncodeFailure panic (JSON has no such number; encode has no error result) — never a silent ""

Algebraic correctness for these is uncontroversial. Effect-tier modules inherit Task's contract (failure surfaced; never panics in well-typed code).

2.10a Sky.Core.Process streaming and Std.Watch (v0.27.0)

Invariants, each with its test (runtime-go/rt/process_spawn_test.go, process_live_test.go, watch_test.go; conformance suite ProcessWatch; corpus Family S surfaces process, watch; the TEA flow rust/crates/sky/tests/process_watch_tea_flow.rs):


3. Std.Ui layout DSL

This is the section the user flagged as "particularly UI libs, so we're certain what goes what and are things mathematically correct".

3.1 Core types

-- sky-stdlib/Std/Ui.sky:54
type Element msg
    = ElText String
    | ElNode LayoutContext (List (Attribute msg)) (List (Element msg))
    | ElLink (List (Attribute msg)) { url : String, label : Element msg }
    | ElImage (List (Attribute msg)) { src : String, description : String }
    | ElButton (List (Attribute msg)) { onPress : Maybe msg, label : Element msg }
    -- ... (full ADT)

-- sky-stdlib/Std/Ui.sky:70
type Attribute msg
    = AttrWidth Length
    | AttrHeight Length
    | AttrPadding Int Int Int Int      -- top right bottom left
    | AttrSpacing Int
    | AttrAlign HAlign VAlign
    | AttrEvent (Event msg)
    | AttrStyle String String          -- CSS prop, value
    | AttrAttribute String String       -- raw HTML attr (data-*, aria-*, ...)
    -- ... (full ADT)

type Length
    = Px Int | Fill Int | Content | Min Int Length | Max Int Length
    | Vh Int | Vw Int

Element msg and Attribute msg are PARAMETRIC on the user's msg type, so event-emitting elements carry their typed handlers all the way to the runtime dispatch. This is the foundation that closes the "untyped event handler" class.

3.2 Layout combinators

CombinatorCSS emittedFile:line
el attrs childdisplay: flex; flex-direction: column; (single child)Ui.sky:219
row attrs csdisplay: flex; flex-direction: row;Ui.sky:224
column attrs csdisplay: flex; flex-direction: column;Ui.sky:229
wrappedRowdisplay: flex; flex-direction: row; flex-wrap: wrap;Ui.sky
griddisplay: grid; grid-template-columns: ...;Ui.sky
gridColumns Ngrid-template-columns: repeat(auto-fill, minmax(Npx, 1fr));Ui.sky
layout attrs el100vh page wrapper + flex column rootUi.sky:1697
layoutWith cfg eladditive: wrapperAttrs reach outer 100vh <div>, rootAttrs apply to rootUi.sky

3.3 The Ui.fill asymmetry (load-bearing invariant)

This is the most-important Std.Ui invariant. Verified at the source level in sky-stdlib/Std/Ui.sky and gated by test/Sky/Build/UiFillCssSpec.hs.

Type:

-- sky-stdlib/Std/Ui.sky:415
fill : Length
fill = Fill 1

fillPortion : Int -> Length
fillPortion n = Fill n

Emission contract (v0.15.55, refined v0.15.56):

PositionCSS emitted
Main-axis fillflex-grow: N; min-{w,h}: 0;
Cross-axis HEIGHT fill (row child)(nothing — relies on flex default align-items: stretch)
Cross-axis WIDTH fill (column / el / textColumn child)width: 100%;
flowchart TD
    Parent[Parent layout context] --> Decide{Child fills which axis?}
    Decide -->|Main axis| Grow["emit: flex-grow: N; min-w/h: 0"]
    Decide -->|Cross HEIGHT in row| Nothing["emit: nothing (default stretch)"]
    Decide -->|Cross WIDTH in column| Width["emit: width: 100%"]

Why the asymmetry: CSS Flexbox §9.8 resolves % against a parent's USED size only when "definite". A flex-grow-derived height is INDEFINITE. Row parents commonly have indefinite heights → the pre-v0.15.55 height: 100% on cross-axis fill collapsed every child to text-content height (issue #63 — three-pane app shell, Input.multiline → 22/51 px). Width keeps 100% because column-parent widths are typically definite AND it survives the [Ui.width fill, Ui.centerX] cascade.

Mathematical claim: Ui.fill is the maximum-monotone element of the Length lattice with respect to the parent's available space. For a row of two Ui.fill children, each gets 50% main-axis. For [Ui.fillPortion 1, Ui.fillPortion 2] the second gets 2/3.

Verification:

3.4 The align-self single-emission invariant

v0.15.56 F4 contract (file Ui.sky):

Test: test/Sky/Build/UiAlignSelfSpec.hs (currently modified per git status — same caveat).

Mathematical claim: rendering is ORDER-INDEPENDENT — swapping the order of two alignment attributes in [Ui.centerX, Ui.alignTop] produces byte-identical output.

3.5 Void-element pseudo-class hoist (v0.15.57 #409)

Problem: pre-v0.15.57, the runtime prepended sky-id-scoped <style> blocks as the FIRST CHILD of the carrying element. Fine for <div> / <button>. SILENTLY DROPPED on void HTML elements (<input>, <img>, <br>, <hr>, ...) — renderVNode skips children for void tags.

Post-v0.15.57: the style block is hoisted to a SIBLING slot immediately after the void element. CSS selector still keys off the void element's sky-id, so the rule applies correctly.

Implication: Input.text [Background.activeColor (...), ...] cfg now correctly applies :active / :hover colours to the <input>.

Verification: a compiler-test spec exists under Sky.Build.UiPseudoClassHoist*Spec (UNVERIFIED at this read — file exists per CLAUDE.md reference; line counts not checked).

3.6 Media-query auto-wrapping

Hover gate — every :hover rule emitted via Background.hoverColor / Font.hoverColor / Border.hoverColor / etc. is AUTO-WRAPPED in @media (hover: hover) by the runtime. This closes the classic mobile "tap-and-stay-hovered" bug.

Reduced-motion gate — every CSS transition (Std.Ui.Transition) and keyframe animation (Std.Ui.Animation) is AUTO-WRAPPED in @media (prefers-reduced-motion: no-preference) by default. Opt out via Transition.attributeUnsafe or respectReducedMotion = False on the Animation Spec ONLY when motion is semantically required.

Implementation at Std/Ui.sky:1183:

"(prefers-reduced-motion: reduce)"   -- runtime checks; spec sees no-preference

Mathematical claim: for any element with hover state H and non-hover state N, on a touch device the runtime presents N (not H stuck-after-tap). For any reduced-motion-preferring user, the runtime presents the keyframe-final state, NOT the animated transition.

3.7 Pseudo-class / transition / animation rule storage

Three parallel mechanisms, identical pattern:

SurfaceMarker attrWhere the rule is emitted
Pseudo-classesdata-sky-pc-rulesUi.sky:1936 (parsed by runtime into <style data-sky-pc="<sid>">)
Transitionsdata-sky-tr-rules + data-sky-tr-respectUi.sky:1949 (<style data-sky-tr=...>)
Animationsdata-sky-anim-rulesUi.sky:1977 (<style data-sky-anim=...>)
Media queriesdata-sky-mq-q + data-sky-mq-rulesUi.sky:1244 (<style data-sky-mq=...>)

Each element gets a sky-id; the runtime expands the marker attr into a sibling <style> whose CSS selector keys off the sky-id. Two elements naming the same animation "fadeIn" with different keyframes don't collide globally because the runtime auto-suffixes the @keyframes name with the element's sky-id-derived ident (fadeIn__r_1_div_0).

3.8 Std.Ui surface catalogue

flowchart TB
    Element[Element msg ADT]
    Element --> Layout[Layout: el / row / column / wrappedRow / grid / paragraph / textColumn]
    Element --> Sized[Sized: link / image / button / input / form]
    Element --> Text[Text: text / none]
    Element --> Raw[Escape: html]

    Attr[Attribute msg ADT]
    Attr --> Sizing[Sizing: width / height / Length: px/fill/content/vh/vw/minimum/maximum]
    Attr --> Padding[Padding: padding / paddingXY / paddingEach / spacing]
    Attr --> Align[Alignment: centerX/Y / alignLeft/Right/Top/Bottom / pointer]
    Attr --> Overflow[Overflow: clip / clipX/Y / scrollbars]
    Attr --> Nearby[Nearby: above/below/onLeft/onRight/inFront/behind]
    Attr --> Event[Events: onClick / onSubmit / onInput / onFocus / onKeyDown / ...]
    Attr --> Style[Style sub-modules]

    Style --> Background[Background: color / image / linearGradient / hover/focus/active/disabled variants]
    Style --> Border[Border: color / width / rounded / solid/dashed/dotted / shadow / inner / hover variants]
    Style --> Font[Font: color / family / size / weight / italic / underline / letterSpacing / hover/focus variants]
    Style --> Region[Region: semantic landmarks → h1..h6, main, nav, aside, footer, aria-*]
    Style --> Input[Input: button/text/multiline/email/password/checkbox/radio/slider]
    Style --> Lazy[Lazy: LRU-cached subtrees]
    Style --> Keyed[Keyed: sky-key for diff identity]
    Style --> Responsive[Responsive: classifyDevice / adapt]
    Style --> Transition[Transition: typed CSS transitions w/ reduced-motion gate]
    Style --> Animation[Animation: keyframe specs w/ reduced-motion gate]
    Style --> Transform[Transform: typed transform property helpers]
    Style --> Grid[Grid: Track ADT (fr/px/auto/minContent/minmax/repeat/repeatAutoFit/repeatAutoFill)]

3.9 Std.Ui correctness verdict

PropertyStatus
ADT exhaustivenessVerified by exhaustiveness checker in compiler
fill asymmetric emissionSpecified at Ui.sky:415 + verified by UiFillCssSpec
align-self single-emissionSpecified F4 contract + verified by UiAlignSelfSpec
Void-element pseudo-class hoistShipped v0.15.57 (#409)
:hover auto-wrapped in @media (hover: hover)Documented + implemented
Transitions/animations auto-wrapped in @media (prefers-reduced-motion: no-preference)Documented + implemented
Sky.Ui → Sky.Tui parity~95% of primitives; gradients/letter-spacing/image-fills emit deduped tuiWarn
Sky.Ui → Sky.Webview paritySame renderer as Sky.Live (WKWebView; macOS only in v0.1)
No raw HTML / no data-sky-evalEnforced — Std.Html escapes everything; eval is forbidden

3.10 Std.Ui known gaps


4. Std.Html + Sky.Live TEA architecture

4.1 Std.Html ADT

File: sky-stdlib/Std/Html.sky:26.

type Html msg
    = HElement String (List (Attribute msg)) (List (Html msg))
    | HText String
    | HRaw String     -- trusted pre-rendered content only

Contract:

Compositional law: text / node / voidNode form the smallest generators. Every other helper (div, span, p, ...) is node "<tag>" partial-applied. So div [] [text "x"] is structurally identical to node "div" [] [text "x"].

4.2 TEA shape (Std.App — App.app + builders)

The unified front door is Std.App: write the TEA record once and compose the optional pieces with App.with… builders. A build-time --target family[:variant] (default web) selects the backend — the web target runs on the Sky.Live runtime documented in the rest of this section.

appDef =
    App.app
        { init = init                   -- () -> (Model, Cmd Msg)
        , update = update               -- Msg -> Model -> (Model, Cmd Msg)
        , view = view                   -- Model -> Element Msg
        , subscriptions = subscriptions -- Model -> Sub Msg
        }
        |> App.withRoutes [ App.route "/" HomePage ] -- URL routing
        |> App.withNotFound HomePage                  -- fallback (MANDATORY for web)
        |> App.withHead headFor                       -- OPTIONAL Model -> List (Html Msg)
        |> App.withRequest onRequest                  -- OPTIONAL Request -> Model -> (Model, Cmd Msg)

main : Task Error ()
main =
    App.run appDef                       -- --target web (default) → runLive

(Use App.web in place of App.app when view : Model -> Html Msg is raw Std.Html rather than a Std.Ui Element.) The init seed is (); the incoming Request (path / query / method / headers / cookies) is delivered through App.withRequest, which runs after init and before the first paint.

Low-level mechanism. App.run lowers to Std.Live's Live.app over a row-poly cfg (extensible records) — routes / notFound / head and the optional consoleAuth (row-poly) and status (i18n) fields. Apps that omit optional fields build byte-identical to the pre-extension shape. User code never imports Std.Live directly; Std.App composes it.

4.3 init lifecycle

flowchart LR
    Req[HTTP request hits /] --> Cookie{live session cookie present?}
    Cookie -->|No| Init["init req called → Model + Cmd Msg"]
    Cookie -->|Yes| Resume["Restore Model from session store; init does NOT run"]
    Init --> Render[Render view → full HTML]
    Resume --> Render
    Render --> SSE[Open SSE channel for patches]

Critical contract: init is per-SESSION, not per-page-reload. Browser reload while session alive → resume from store. Force fresh init with Cmd.perform (Live.endSession ()) (\_ -> Ignore): the session ends, its tabs reload, and the old session cookie is never adopted again.

req shape (v0.16.7 #417 + v0.16.8 #423):

FieldTypeNotes
req.pathStringURL path
req.queryStringRaw query string (use Sky.Core.Http.parseQuery)
req.paramsDict String StringMatched :name route segments
req.methodString"GET" / "POST" / ...
req.headersDict String StringCanonical-case
req.cookiesDict String StringParsed

Adding ANY of these fields to a user req access pattern is row-poly safe — pre-v0.16.8 apps that read req.path continue to build.

4.4 Cmd / Sub algebra

Std.Cmd (sky-stdlib/Std/Cmd.sky:36-83):

type Cmd msg                                   -- opaque

none      : Cmd msg                             -- identity
batch     : List (Cmd msg) -> Cmd msg           -- associative + commutative monoid w/ none
perform   : Task e a -> (Result e a -> msg) -> Cmd msg
publish   : String -> any -> Cmd msg            -- pub/sub (echo by default)
publishNoEcho : String -> any -> Cmd msg        -- pub/sub (skip publisher's subscription)

Std.Sub (sky-stdlib/Std/Sub.sky:14-51):

type Sub msg                                    -- opaque

none           : Sub msg
every          : Int -> msg -> Sub msg          -- tick every N ms
batch          : List (Sub msg) -> Sub msg
subscribeTopic : String -> (any -> msg) -> Sub msg

Monoid laws on Cmd.batch and Sub.batch:

4.5 SSE patch protocol

sequenceDiagram
    participant Browser
    participant Server as Sky.Live runtime
    participant Update as user update fn

    Browser->>Server: GET / (full HTML)
    Server-->>Browser: HTML + open SSE channel /_sky/sse
    Server->>Server: heartbeat every 15s
    Note over Server: hello handshake with seq=0
    Browser->>Server: POST /_sky/event (typed Msg payload)
    Server->>Update: dispatch Msg, get (Model', Cmd)
    Server->>Server: diff(prevVNode, view Model')
    Server-->>Browser: SSE event "patch" with VNode patch JSON
    Server->>Server: execute Cmd in goroutine
    Note over Server,Update: If Cmd.perform completes → new Msg → another diff

Hardening rules (documented in CLAUDE.md "Reverse-proxy hardening"):

XSS hardening (#338 / C9): __skyReviveScripts allowlist drops event handler attributes (onclick, onerror, ...). Re-injection of scripts post-patch goes through this gate.

Input preservation (3 failure modes closed):

  1. Empty patches JSON-ack (don't HTML-fallback) → preserves uncontrolled fields like password.
  2. Full-body swap preserves EVERY uncontrolled INPUT/TEXTAREA/SELECT (not just document.activeElement).
  3. Open <select> defence — __skyApplyPatches skips patches targeting focused/contained selects.

4.6 URL routing + history

4.7 Wire-event arg shapes (mandatory contract)

EventElement typeArgs
click/focus/blur/mouseover/outany[]
input/changecheckbox[checked : Bool]
input/changeradio[checked : Bool] — use onClick per choice instead
input/changenumber / range[value : Float]
input/changetext / textarea / select[value : String]
submitform[formData] — Dict String String OR typed record
keydown/keyup/keypressany[key : String]

4.8 Password forms — mandatory pattern

Rule: Use onSubmit with form data; NEVER onInput per keystroke on password fields.

type alias AuthCreds = { email : String, password : String }
type Msg = UpdateEmail String | DoSignIn AuthCreds

view model =
    form [ onSubmit DoSignIn ]
        [ input [ type "email", name "email", value model.email, onInput UpdateEmail ] []
        , input [ type "password", name "password" ] []   -- no value, no onInput
        , button [ type "submit" ] [ text "Sign in" ]
        ]

Three reasons (CLAUDE.md):

  1. Password managers watch DOM mutations on password inputs; every server-driven re-render with value= triggers re-prompt.
  2. Secret never lives in Model → never serialised into session store.
  3. Form submit reads live DOM value, not debounced keystrokes.

The DoSignIn AuthCreds constructor takes a typed record. The wire driver decodes form data directly into the Go struct via case-insensitive json.Unmarshal. No per-Msg decoder boilerplate.

4.9 Sky.Live correctness verdict

PropertyStatus
TEA shape (init/update/view/subs) is totalVerified by HM type-checker
Cmd/Sub none/batch monoidHeld by inspection; compiler-test specs cover batch
SSE patch idempotence__skyApplyPatches is idempotent on no-op patches
Input preservation across re-rendersClosed via 3 documented mechanisms (C1 residuals)
XSS-resistant __skyReviveScriptsAllowlist + event-handler stripping (#338)
Session restore on browser reloadVerified — init does NOT run on resume
URL routing — literal before patternDocumented; matching is by-declaration-order
Password-form anti-pattern enforcementDocumented; not statically checked (UNVERIFIED)
Production gate (ENV != dev) closes console/banner/metricsImplemented in productionFromEnv(); v0.15.43 panic gate

4.10 Sky.Live known gaps


5. Std.Db + Std.Auth

5.1 Std.Db typed parameter binding (v0.16.26)

File: sky-stdlib/Std/Db.sky (551 lines).

Type ADT at Db.sky:349-358:

type SqlValue
    = SqlString String   -- TEXT / VARCHAR / CHAR / UUID-as-text / JSON-as-text
    | SqlInt Int         -- INTEGER / SMALLINT / BIGINT / SERIAL
    | SqlFloat Float
    | SqlBool Bool
    | SqlBytes String    -- BLOB / BYTEA (raw bytes as Sky String)
    | SqlDecimal Decimal -- arbitrary-precision NUMERIC / DECIMAL
    | SqlTime Time       -- TIMESTAMP / TIMESTAMPTZ
    | SqlMoney Money     -- TEXT "ISO_CODE AMOUNT" (paired with Db.Decode.money)
    | SqlNull SqlValue   -- typed NULL — wrapped variant is the type-witness

Nullability invariant: SqlNull (SqlInt 0) means "NULL, Int type". The wrapped value is the WITNESS — its Sky value is discarded; only the driver needs the type tag.

fromMaybe* helpers at Db.sky:367+:

fromMaybeString : Maybe String -> SqlValue
fromMaybeString m =
    case m of
        Just v  -> SqlString v
        Nothing -> SqlNull (SqlString "")

8 variants cover String / Int / Float / Bool / Bytes / Decimal / Time / Money.

SqlField ADT (PATCH semantics):

type SqlField = SetField SqlValue | OmitField

Used by Db.updateFields conn table whereCols setFields to generate dynamic UPDATE that includes only SetField columns. Identifier allowlist rejects characters outside [A-Za-z0-9_.].

5.2 Std.Db security invariants

5.3 Std.Db migrations

Db.migrate (Db.sky) implements versioned forward-only schema migrations:

5.4 Std.Auth contract

File: sky-stdlib/Std/Auth.sky (122 lines).

register : Db -> String -> String -> Task Error Int       -- email, password
login    : Db -> String -> String -> Task Error Int
setRole  : Db -> Int -> String -> Task Error ()

hashPassword     : String -> Result Error String          -- bcrypt cost 10
hashPasswordCost : String -> Int -> Result Error String   -- bcrypt cost N
verifyPassword   : String -> String -> Result Error Bool
passwordStrength : String -> Result Error Int

signToken           : Secret -> a -> Int -> Result Error String   -- a is encodable
verifyToken         : Secret -> String -> Result Error Value      -- the claims as Json.Value
signTokenWithClaims     : Jwt.Algorithm -> Jwt.Claims -> Result Error String
verifyTokenWithAlgorithm : Jwt.Algorithm -> Int -> String -> Result Error String

Security invariants:

Verification:

5.5 Std.Db / Std.Auth verdict

PropertyStatus
SqlValue covers full driver type range9 variants (closed v0.16.26)
Identifier allowlist on dynamic SQLSpecified + enforced
Migration checksum gateImplemented in migrate
signToken secret is typed StringEnforced; v0.15.44+ contract
Bcrypt password hashingDefault cost 10; adjustable
Tenant isolation in console storev0.16.6 SQL-layer enforcement
Argon2id / X25519 / Ed25519NOT shipped — documented gap
JWE (encrypted JWT)NOT shipped — only JWS
Migration rollbackNOT supported (forward-only by design)

6. Cross-backend parity

flowchart LR
    SkySrc[Same view function: Model -> Element Msg]
    SkySrc --> Live[Sky.Live: WebKit/Chromium]
    SkySrc --> Tui[Sky.Tui: ANSI cells]
    SkySrc --> Webview[Sky.Webview: WKWebView]

    Live -->|HTTP + SSE| LiveR[liveAppRun + session store]
    Tui -->|Goroutine + raw TTY| TuiR[Tui runtime + uniseg]
    Webview -->|In-process Bind/Eval| WvR[webview_go]
FeatureSky.LiveSky.TuiSky.Webview
Std.Ui layout primitivesFull~95%Full (mirrors Live)
Background gradients / image fills✓warn✓
Font letter-spacing✓warn✓
Pseudo-classes (:hover / :focus)✓warn✓
Transitions / animations✓warn✓
Media queries✓ignored✓
Std.Live.Head per-page <head> injection✓n/a✓ (head ignored — single shell window)
URL routing + history✓n/a✓ (in-process navigation only)
SSE diff patches✓✓ (cell diff)✓ (in-process)
Session store (memory/sqlite/redis/...)✓n/an/a (single process)
Cmd.perform✓✓✓
Cmd.publish / Sub.subscribeTopic✓✓✓
Production gate (ENV != dev)✓n/an/a
TTY signal teardown (SIGTERM/SIGHUP/SIGQUIT/SIGINT)n/a✓✓ (WKWebView lifecycle)
Backend OS supportAny (Linux/macOS/Win Go)Any TTYmacOS only in v0.1
cgo requiredNoNoYes (sky build auto-detects rt.Webview_app)

Identical view function — the same Model -> Element Msg produces three byte-identical paint results modulo style primitives that one backend does not implement (Sky.Tui silently ignores <style>; Sky.Webview honours media queries identically to Sky.Live).


7. Correctness verdict

7.1 Per-module verdict

ModuleMathematicalStructuralVerified byVerdict
Sky.Core.MaybeFunctor/App/Monad laws hold by inspectionn/aInspection + sweepSOLID
Sky.Core.ResultFunctor/Bifunctor/Monad laws holdn/aInspection + sweepSOLID
Sky.Core.ListFunctor + fold laws + constant-stack contractn/aPer-op CPS specSOLID (v0.17 close)
Sky.Core.Dict/SetFinite map/set lawsStable iterationSweepSOLID
Sky.Core.StringRune-aware round-tripsn/aSmoke testSOLID-mostly (grapheme gap)
Sky.Core.MathIEEE 754 via Go mathn/aInspectionSOLID
Sky.Core.CryptoAEAD + constant-time-equaln/art/crypto_aead_test.go + crypto_sign_kx_test.goSOLID-mostly (no Argon2id)
Std.Crypto.Sign/Kx/KdfRFC 8032 / 7748 / 5869 vectors + low-order refusalRedacting key typesrt/crypto_sign_kx_test.go + conformanceSOLID
Std.Crypto.NoiseCacophony IK vectorsSingle-use state valuesrt/noise_test.go + conformanceSOLID
Std.Crypto.CpaceDraft B.1 vectorsSingle-use Pendingrt/cpace_test.go + conformanceExperimental: awaiting external review
Std.QrModule-exact vs an independent encoderPure, TinyGo-cleanrt/qr_test.go + conformanceSOLID
Sky.Core.JwtSignature-then-claims + exp/nbfalg: none rejected by ADTrt/jwt_test.goSOLID
Sky.Core.TaskMonad laws + effect tier disciplinePanic gate v0.15.43rt/task_test.go + retry_test.goSOLID
Std.Uin/a (DSL)fill asymmetry + align-self single-emission + pseudo-class hoist + media-query auto-wrapUiFillCssSpec + UiAlignSelfSpec + 39-example sweepSOLID (v0.15.55-57 close)
Std.HtmlCompositional generatorsEscape contractInspectionSOLID
Std.Live runtimeTEA shape + Cmd/Sub monoidSSE + XSS hardening + input preservationMany compiler-test specs + 39-example sweep + PlaywrightSOLID-with-caveats (password rule docs-only)
Std.Dbn/aSqlValue + tenant SQL gate + migration checksumrt/db tests + v0.16.6 gateSOLID (v0.16.26 close)
Std.Authn/aTyped secret + bcrypt + JWT exp/nbfrt/auth_test.goSOLID-with-gap (no Argon2id)

7.2 Overall verdict

SOLID across the stdlib surface area with three documented caveats:

  1. Std.Ui correctness is verified at the EMISSION level (UiFillCssSpec, UiAlignSelfSpec) but visual-regression is by the 39-example sweep, not pixel-diff. A CSS-engine regression in Chromium/WebKit/Firefox could break user code without Sky noticing.
  2. Stdlib algebraic laws (Functor / Monad on Maybe / Result / List / Task) are verified BY INSPECTION, not by property test. Adding QuickCheck-style law specs is documented as a future gap.
  3. Password-form rule is documentation, not a static check.

The "no runtime panic from well-typed Sky code" non-regression rule (CLAUDE.md Rule 8) is enforced by:


8. Critical gaps + pending regression tests

Listed in priority order. Each is actionable — there's a clear implementation step OR a clear spec to write.

8.1 High priority

G1. Algebraic-law property specs (Maybe / Result / List / Task).

G2. Grapheme-cluster API on Strings.

G3. Limitation #8 regression sweep at 1M elements.

G4. Password-form static check (LSP lint).

8.2 Medium priority

G5. Argon2id password hash variant.

G6. Ed25519 / X25519 in Crypto module. CLOSED in v0.27.0: Std.Crypto.Sign (Ed25519), Std.Crypto.Kx (X25519, low-order refusal) and Std.Crypto.Kdf (HKDF-SHA256), with RFC vectors (§2.7a).

G7. Cmd.batch ordering specification.

G8. Sky.Tui pseudo-class / animation gating.

8.3 Lower priority

G9. JWE (encrypted JWT).

G10. JWK / JWKS discovery for RS256.

G11. Cross-process Lazy cache.

G12. Migration rollback (DOWN scripts).


9. Read this together with the compiler doc

The companion docs/rust-rewrite/ (07-lowering-and-ir.md, 08-go-codegen.md, 09-runtime-and-ffi.md, and 14-runtime-narrowing-taxonomy.md for the narrowing floor) explains:

The two documents together cover the full pipeline:

flowchart LR
    Source[Sky source] -->|This doc: stdlib surface contract| Stdlib[Stdlib usage]
    Source -->|Compiler doc: parsing / canon / HM / lowering| Codegen[Go IR + runtime]
    Stdlib --> Codegen
    Codegen --> Output[Typed Go binary + runtime]

When a user asks "is this Sky code mathematically correct?", route to THIS doc. When they ask "how does Sky compile this code?", route to the compiler doc.


Last updated: 2026-06-23. Compiler tracked: v0.16.6 RC + v0.17 in flight on feat/v0.17-fully-typed-codegen.