Known limitations (v0.27.x)
Active limitations users still hit at HEAD. Each entry explains the gap,
why it exists, and the workaround. Closures across the v0.15 / v0.16 /
v0.17 cycle (parametric record aliases, polymorphic re-instantiation,
wildcard-any soundness, panic-class hardening, head-alias unfolding,
auto-TCO for all list ops, negative literal args, multi-line signatures,
zero-arg call shapes, Css keyword constants, FFI interface satisfaction,
Dict typed-key inference, Sky.Live init request shape, URL-driven route
Navigate Msg) are recorded in CHANGELOG.md and the per-version archives
under docs/history/. This file lists ONLY what's still active at HEAD.
Language (design floor — by intent)
-
No higher-kinded types. No
Functor/Monad/Applicativeclasses. Sky's type system is Hindley-Milner (intentional). Use concrete types and explicitandThen/mapper ADT. -
No
whereclauses. Uselet…ininstead. Intentional — one bindings construct, two would be a needless surface. -
No custom operators. Only built-in operators (
|>,<|,++,::, arithmetic, comparison). Intentional — reading other people's code stays predictable. -
CLOSED — row-polymorphic annotations parse.
{ r | field : T }works in annotations and in aliases (type alias Named r = { r | age : Int }), and the checker keeps the other fields of the record.
Compiler (defensive bounds)
-
HM type-checker heap budget on monolithic Std.Ui-heavy modules. For very large monolithic view files (~25+ polymorphic
Element Msghelpers + many nested calls) the constraint solver can grow O(N²) in heap. There is no cap. This entry used to say the compiler "defensively caps solver invocations atSKY_SOLVER_BUDGETsteps (defaultmax(5,000,000, constraint_count × 200))" and aborts withTYPE ERROR: constraint solver exceeded budgetrather than OOMing the host. That fence was the retired Haskell solver's (Solve.hs:708-746); it was not carried into the Rusttycrate and the entry was never updated.$ grep -rn 'bump_step\|SKY_SOLVER_BUDGET' rust/crates $ # nothingThe only surviving mention anywhere is a Go comment noting the knob "is read by the Haskell compiler" (
runtime-go/rt/env_prefix.go:24). So: neitherSKY_SOLVER_BUDGETnorSKY_SOLVER_BUDGET_FACTORis read, there is nobump_step, and a constraint-explosion module OOMs the host — it does not abort with a diagnostic. Treat the workaround below as the only mitigation until a budget is reinstated.Workaround: split heavy view modules across multiple files (per
examples/19-skyforum's 8-module pattern —State.skyholds types only,Update.sky/View/Common.sky/ one View module per page /Main.skydispatcher).
rt.Coerce residual surface (documented sound — not a soundness gap)
-
rt.Coerce-family narrowing calls remain at typed boundaries (sealed-iface ctor narrowing, parametric record alias, typed list, container, primitive, tuple, map/dict, generic-param erasure). 446 call sites on the canonicalexamples/26-ui-showcasebenchmark —26-ui-showcase adapter=0 dispatch=0 narrow=446,rust/crates/xtask/coerce_floor.golden:73, which is the gate's own census over exactly thisrt.Coerce/rt.As*/rt.Field/rt.SkyCallset.This entry said 214, and the next sentence said #677 "would drop ~476 of these sites" — more sites than the count it had just given. The 214 was never re-derived after the golden landed. Quote the golden, not a remembered number:
grep 26-ui-showcase rust/crates/xtask/coerce_floor.golden. All sites are documented sound with explicit per-class soundness proofs indocs/history/v0.17/rt-coerce-residual-surface.md. The synchronous-panic gate (defer rt.LogPanicAndExit()) catches any panic that does fire and routes it to anErr-classified clean exit. Sealed-interface ADT emission (#677) would drop ~476 of these sites further; deferred to v0.17.x / v0.18.0 per the v0.17.0 release plan.
Sky.Live + Std.Ui (active items tracked for v0.17.x)
SKY_LIVE_BASE_PATHmounted sub-apps share session-store namespace. When two Sky.Live apps mount under the same parent, they currently share the parent's session ID space (thesky_sidcookie). For multi-tenant deployments use separate cookie names per sub-app via the[live]cfg.
Stdlib
-
Dictkeys must be a primitive to survive iteration. ADict k vis a Gomap[string]Vat runtime: every key is encoded to a string on the way in. Lookup (get/member/insert/remove) encodes the probe the same way and therefore works for ANY key type, but the operations that let the key back OUT —toList,keys,values,foldl,map— have to decode it.String,Int,Float,CharandBooldecode, and they do so wherever they are used, including inside a key-polymorphic helper (f : Dict k v -> …), because the encoded key carries its own type tag. One case does not:- Composite keys (tuple, list, record, custom type) — now a
COMPILE-TIME rejection,
[E2008], not a runtime failure. Their stringification is not injective —("a b", "c")and("a", "b c")both render{a b c}— so no decoder could be correct, and a defect that can never be fixed downstream belongs to the type checker rather than to the runtime.sky check(and the LSP, live in the editor) refusesDict ( Int, Int ) vwith the offending key type, the five that do work, and the workaround; the oldUnsupportedDictKeypanic is now unreachable from code that type-checks. The check resolves through type aliases (type alias Coord = ( Int, Int )is rejected too) and fires only when the key type is CONCRETE — a key-polymorphicDict k vis ordinary Sky and stays accepted, with a call site that instantiateskto a composite rejected at that call site. Workaround: key by a primitive rendering of the composite (e.g.String.fromInt a ++ ":" ++ String.fromInt b) and keep the structured form in the value, or hold the entries as aList ( k, v )of pairs if you do not need lookup.
Making that one WORK would mean changing what a composite key is encoded as, not how it is decoded — a wider change than the decode side. It is closed in the other direction instead: rejected at check time, so it can no longer reach the runtime at all.
- Composite keys (tuple, list, record, custom type) — now a
COMPILE-TIME rejection,
-
CLOSED in v0.27.0 — recursion through
Task.andThenis stack-safe. Before v0.27.0 eachandThenstep nested Go frames (AnyTaskAndThenforced the outer task and then the continuation's task inside the same Go call), socount n = if n == 0 then Task.succeed 0 else Task.succeed n |> Task.andThen (\_ -> count (n - 1))died at two million iterations with the fatal
goroutine stack exceeds 1000000000-byte limit. A Task is now data (rt.SkyTaskholds a task node) run by one interpreter (runtime-go/rt/task_trampoline.go), which pops a bind before it runs the task the continuation returns. The loop above, recursion throughTask.onError, andTask.sequenceover a long list run at a constant Go stack (regression tests:runtime-go/rt/task_trampoline_test.goat a 1 MB stack cap, andrust/crates/sky/tests/task_trampoline_flow.rsend to end). What remains, by design:- A recursive call that is not behind a continuation,
step n = step (n - 1) |> Task.map f, is evaluated while the task is BUILT. That is ordinary strict recursion and grows the Go stack as it does in Elm; put the recursive call insideandThen. - A recursion that maps its own result,
\_ -> step (n + 1) |> Task.map f, keeps one pendingmapper level until the innermost step finishes. Those frames live on the heap, not the Go stack, and the semantics require them.Task.loop(explicit state) andTask.forever(a service loop) stay the clearest forms for a long loop.
- A recursive call that is not behind a continuation,
Sky.Spa
Sky.Spa (client-side TEA compiled to wasm) is a supported feature; these are its
current scope boundaries — documented so they are not mistaken for bugs. Full
context: docs/skyspa/overview.md + docs/skyspa/design.md.
-
Web bundle weight → desktop/mobile-embed only. A real Sky.Spa app is a standard Go→wasm bundle of ~9.5 MB raw / ~2.5 MB gzip (
examples/60-spa-todos, measured) — fine for a one-time desktop/mobile-embed download, too heavy for production web (Elm ≈30 KB). Inherent to Sky's reflection-native dispatch (sky_call/reflect.MakeFunc). -
Production web (TinyGo / Sky→JS) = v2. TinyGo cannot compile
reflect.MakeFunc, so shrinking the bundle needs a reflection-free core or a Sky→JS backend — both are v2, not built (docs/skyspa/design.md§0/§9). -
The auto-derived client/server split is built.
sky buildon aSpa.appentry or aweb:apptarget splits the app into a wasm client and a native backend (docs/skyspa/auto-split.md). What the split cannot carry (a server arm it cannot read, two wire records of one name, a model field with no wire form) is a build error, andsky checkreports the same error. -
Client effect surface is bounded. Client effects run through a single-threaded wasm interpreter:
Cmd.perform(sync kernels inline; asyncHttpviafetch) andSub.everytimers only.Cmd.publishis a documented client no-op (no session bus in a single tab);Sub.subscribeTopicstreams from the backend's/_sky/subendpoint, andSky.Core.WebSocketuses the browser WebSocket API;Http.Streamsubscriptions are not wired on the client. -
Browser coverage. The client runs in Chromium and WebKit in the release gate's browser e2e scripts (
scripts/spa-*-e2e.sh); other browsers are not tested.
Roadmap (not active bugs, just deferred)
-
Install-time Go-binding generation deferral.
sky installcurrently emits the full.skycache/go/<pkg>_bindings.go(Stripe: 76k FFI symbols, ~330k lines). A future build-time, reachable-only generation pass would drop Stripe install from ~8 min to ~10 s. -
Sub-app Sky-side API.
MountSubAppis currently Go-side (rt.MountSubAppin generatedmain.go). A Sky-side sub-apps builder onStd.App(e.g.App.withSubApps [...]composed ontoApp.app) is on the v0.17.x list. -
Lambda-typed OUTPUT for ALL call sites. Typed routing for
List.map/Maybe.mapetc. usesrt.List_mapT[A, any]— input typed, outputany. ForcingBto concrete would need per-call-site monomorphisation that doesn't conflict with Sky's curry shape. -
Sealed-interface ADT emission (#677). Would drop the rt.Coerce/AsListT floor by ~75 % on UI-heavy examples. Multi-session work per CLAUDE.md §0.2 N-strikes circuit-breaker (3 prior swap attempts produced regressions — requires re-classification before a 4th attempt). Deferred to v0.17.x patches or v0.18.0.
What was closed in v0.17.0
(Reference for users upgrading from v0.16.x.)
- Negative literal arguments (
f -1parses correctly asf (-1)) - Multi-line function signatures (both
: Tand-> Tcontinuation) - Zero-arg call shape arity gate (StrictHmArityGate, code
[E2007]) Css.*keyword constants are bare values (Css.zeronotCss.zero ())Dict.toListtyped-key inference works inline AND let-boundsky checkempirically validates Go interface satisfaction- Non-tail-recursive list operations now CPS / accumulator-rewritten (13/13 List/Maybe/Result list ops on constant Go stack)
- 3-tuple literals at top-level parse correctly
- Sky.Live
initreceives fullRequest(path / query / method / headers / cookies) - URL-driven route matches fire
NavigateMsg
Full history in CHANGELOG.md + docs/history/v0.17/. (This line named
docs/archive/v0.17-design-notes/; no docs/archive/ tree exists — frozen
material lives under docs/history/.)