sky-lang.org · Examples · Docs
Experimental — Sky is an opinionated, AI-friendly programming language under active development. APIs and internals may change between minor versions.
Sky is a fullstack programming language that combines Go's pragmatism with the elegance of pure-functional, ML-family languages. Write functional, strongly-typed code with a batteries-included stdlib — Sky.Live for server-driven UI, Sky.Tui for terminal UI (sharing the same Std.Ui code), Std.Db for SQL persistence, Std.Auth for sessions, Sky.Core.Error for unified error handling — import any Go package with auto-generated FFI bindings (no hand-written glue), and ship a single portable binary. Sky's explicit types, exhaustive pattern matching, and strict Task effect boundary make it AI-friendly by design: both humans and LLMs tend to write code that compiles the first time.
module Main exposing (main)
import Std.Log exposing (println)
main =
println "Hello from Sky!"- Type-directed lowering throughout (v0.15). Sub-expressions at lambda bodies, record-field inits, list elements, and call args lower with the slot's typed Go form propagated. Closes the long- standing parametric-record-alias bug class (callback fields keep their typed callee parameter; cross-alias passing works without the alias-chain workaround; inline lambdas in record fields keep their typed shape). Architecture write-up: docs/v1-rfc/type-soundness-deep-analysis.md.
- Go generics on parametric record aliases (v0.15). A
type alias Cfg msg = { onSubmit : msg, label : String, ... }now emitstype Cfg_R[T1 any] struct { OnSubmit T1; Label string; ... }with per-instance type args. Stripe-SDK-scale benchmark (examples/13-skyshop, 76 k FFI symbols) still tree-shakesmain.go14 k → 4 k lines (−71 %) andstripe_bindings.go326 k → 58 k lines (−82 %). - Same-module polymorphic re-instantiation (v0.15). Annotated
f : Cfg msg -> msgcalled withmsg=IntANDmsg=Boolin the same module both work — sibling references alpha-rename per call site. Wildcard-anysigs stay on the shared-env path so body ↔ caller unification chains keep soundness. - Layer 3 stdlib — every kernel module is Sky source (carried
forward from v0.14). Browse via
sky-stdlib/{Sky/Core,Std,Sky/Http}/*.sky, orsky doc --servefor a browsable HTTP doc server with type-signature search (Hoogle-style), in-module symbol filter, and Markdown rendering. - Auto-TCO. Every Sky function with tail-position self-recursion
compiles to a
for { ... continue }Go loop. Constant Go stack regardless of input size. Applies to user code, not just stdlib. - Sky Console + sub-app mount + observability federation. Every
Sky.Live / Sky.Http.Server app auto-mounts a Std.Ui dashboard at
/_sky/consolein dev mode. Prometheus metrics at/_sky/metrics(Bearer-gated in production).rt.MountSubApphosts any Sky binary (or any HTTP server) under any URL prefix; logs / metrics / spans push back to the parent for one-scrape observability across the tree. - LSP, dev tooling. Hover + goto-def for every USED symbol class.
sky watch(file-watch rebuild + restart with sticky-on-error policy),sky doctor(project + env health checks),sky console(standalone Std.Ui dashboard — Live or Tui backend). - 27 example projects covering CLI, Sky.Tui, Sky.Live + Sky.Http apps, databases (SQLite / PostgreSQL / Firestore), payments (Stripe), auth, GUI (Fyne), a Reddit/HN-style forum, and a visual-regression Std.Ui showcase.
- Go compilation target — fast builds, single static binary, access to the full Go ecosystem (databases, HTTP servers, cloud SDKs).
- Pure-functional ML-family front-end — Hindley-Milner type inference, algebraic data types, exhaustive pattern matching, pure functions, model/update/view/subscriptions architecture (TEA).
- Server-driven UI — DOM diffing, SSE subscriptions, session management on the server. No client-side framework. (Same architectural style popularised by Phoenix LiveView; design + implementation independent.)
Sky compiles to Go. One binary runs your API, DB access, and server-rendered interactive UI — one codebase, one language, one deployment artifact.
Sky's surface syntax is deliberately compatible with the Elm language (BSD-3-Clause, © Evan Czaplicki and contributors) and several files in the type-inference core are derivative works adapted from elm/compiler. Full attribution + licence text in NOTICE.md.
The compiler is in Haskell (GHC 9.4+). Single sky binary. Runtime
in Go (runtime-go/rt/), embedded into the binary via Template
Haskell — no separate install. See
docs/compiler/journey.md for the TS → Go →
self-hosted Sky → Haskell history.
Six killer modules cover the common needs of any modern web app — no
plugins, no separate services, no npm install.
type Msg = Increment | Decrement
update msg model =
case msg of
Increment -> ( { model | count = model.count + 1 }, Cmd.none )
Decrement -> ( { model | count = model.count - 1 }, Cmd.none )
view model =
div []
[ button [ onClick Increment ] [ text "+" ]
, span [] [ text (String.fromInt model.count) ]
, button [ onClick Decrement ] [ text "-" ]
]Full TEA loop (init / update / view / subscriptions), async work via
Cmd.perform, persistent sessions across deploys (memory / SQLite /
Redis / Postgres / Firestore), input-authority protocol that protects
the user's typed value across re-renders, reverse-proxy-hardened SSE
with auto-reconnect + retry queue. See
Sky.Live overview.
import Std.Ui as Ui
import Std.Ui.Background as Background
import Std.Ui.Font as Font
view model =
Ui.layout []
(Ui.row
[ Ui.spacing 12, Ui.padding 16, Background.color (Ui.rgb 255 102 0) ]
[ Ui.button [] { onPress = Just Decrement, label = Ui.text "−" }
, Ui.el [ Font.size 24, Font.bold ] (Ui.text (String.fromInt model.count))
, Ui.button [] { onPress = Just Increment, label = Ui.text "+" }
])Build a UI from typed primitives (row, column, el, paragraph,
textColumn, link, image, button, input, form, html) and
typed attributes from focused sub-modules (Background.color,
Border.rounded, Font.size, Region.heading, …). Renders to
inline-styled HTML with semantic tags dispatched from Region.*
(<h1..h6>, <main>, <nav>, <aside>, <footer>). Forms with the
password best-practice pattern (Ui.form + Ui.onSubmit decoding
formData into a typed record — secret never enters Model). File /
image upload with browser-side resize hints. Same source code runs in
both Sky.Live (browser) and Sky.Tui (terminal — see below). See
Sky.Ui overview. Prior-art attribution:
NOTICE.md.
-- shared.sky — both Live and Tui share this view + update
view model =
Ui.column [ Ui.spacing 8, Ui.padding 16 ]
[ Ui.el [ Font.bold, Font.size 24 ] (Ui.text (String.fromInt model.count))
, Ui.row [ Ui.spacing 4 ]
[ Ui.button [] { onPress = Just Decrement, label = Ui.text "−" }
, Ui.button [] { onPress = Just Increment, label = Ui.text "+" }
]
]-- Main.sky (web) -- MainTui.sky (terminal)
main = Live.app cfg main = Tui.app cfg |> Task.runSame update semantics, same view widgets, two completely different
output targets. Sky.Tui handles bracketed paste, wide chars (CJK +
emoji + ZWJ), focus rings, scroll wheel, mouse press, viewport pixel
canvas (1280×720 logical px maps to cells), resize via SIGWINCH. See
Sky.Tui overview and
examples/22-tui-stopwatch-ui for a
stopwatch in <100 lines that runs in both backends.
Auth.register db "alice@example.com" password
|> Task.andThenResult (\uid ->
Auth.signToken secret (Dict.fromList [("sub", String.fromInt uid)]) 86400)bcrypt password hashing, HMAC-SHA256 JWTs, plus optional DB-backed
register / login / setRole. Minimum-32-byte secret enforcement,
constant-time compare, configurable cost. See
Std.Auth overview.
Db.withTransaction db (\tx ->
Db.exec tx "UPDATE accounts SET balance = balance - ? WHERE id = ?" [amount, fromId]
|> Task.andThen (\_ ->
Db.exec tx "UPDATE accounts SET balance = balance + ? WHERE id = ?" [amount, toId]))Parameter-safe queries, transactions, conventional CRUD helpers
(insertRow / getById / updateById / deleteById /
findOneByField / findManyByField / findByConditions). Switch
driver in sky.toml; never touch it again in your code. See
Std.Db overview.
Every Sky.Live / Sky.Http.Server app ships with:
| Surface | What it is |
|---|---|
🔍 Console link |
Floating bottom-right anchor injected into every dev-mode page. Same-origin link to /_sky/console. |
/_sky/console |
Bundled Std.Ui dashboard reverse-proxied behind your app. Tabs: Overview · Metrics · Logs · Traces · Errors. Auto-aggregates everything from your app + every mounted sub-app. |
/_sky/metrics |
Prometheus scrape endpoint (Bearer-gated in production). sky_live_requests_total{route,status}, sky_live_request_seconds, custom counters via rt.RecordCounter. |
/_sky/healthz / /_sky/readyz / /_sky/buildinfo |
k8s / Cloud Run probes + build metadata. |
| Structured logs | Log.info / .warn / .error / .infoWith with level + message + request-correlation ID; HTTP access log automatic. |
| Trace spans | Every HTTP request opens a span; rt.RecordTrace adds children. Exports to OpenTelemetry if OTEL_EXPORTER_OTLP_ENDPOINT is set. |
Sub-app mount — host multiple Sky apps under one binary, with federated observability:
// Inside your parent Sky app's generated main.go
rt.MountSubApp(mux, "/billing", rt.SpawnBinary("./billing-app"))
rt.MountSubApp(mux, "/admin", rt.SpawnBinary("./admin-app"))
rt.MountSubApp(mux, "/docs", rt.SpawnBinary("./hugo-server"))Each sub-app runs as its own child process — own session store, own
update loop, own cookies, zero shared state. The reverse proxy gives
the user a single port and origin. Observability federates
automatically: every log / metric / span the child emits gets pushed
back to the parent labelled subapp="billing", so one Prometheus
scrape on the parent covers the whole tree.
sky run # dev — console, banner, logs/metrics on
ENV=production sky-out/app # prod — console gone, /_sky/metrics behind Bearer auth-- Exact arithmetic — 0.1 + 0.2 = 0.3 genuinely
total = Dec.add (Dec.fromString "0.1" |> okOr Dec.zero)
(Dec.fromString "0.2" |> okOr Dec.zero)
-- Currency-typed Money — USD vs JPY rejected at compile time
subTotal = Money.fromMajor Money.USD 100
tax = Money.percentOf (Dec.fromString "8.875" |> okOr Dec.zero) subTotal
total = Money.add subTotal tax -- "$108.88"
-- Fair-split invoice — sums to $100 exactly
parts = Money.allocate 3 (Money.fromMajor Money.USD 100)
-- → [$33.34, $33.33, $33.33]
-- Timezone-aware (no /usr/share/zoneinfo needed; embedded tzdata)
nextMonth = Stime.addMonths 1 today -- Jan 31 + 1 → Feb 28/29 clampedDecimal backed by shopspring/decimal; Money enforces currency-
match at the type level; Std.Time ships embedded time/tzdata. ISO
4217 enum covers 50+ codes + crypto (BTC, ETH, USDT, USDC). Full
surface: Standard library reference.
# macOS / Linux — single-binary install
curl -fsSL https://raw.githubusercontent.com/anzellai/sky/main/install.sh | sh
# or with Docker
docker run --rm -v $(pwd):/app -w /app anzel/sky sky --helpPrerequisite: Go 1.21+ — Sky compiles to Go.
sky init hello
cd hello
sky run src/Main.sky # build + run
sky watch src/Main.sky # rebuild + restart on save
sky doc --serve # browsable API docs (any browser)
sky doctor # health checksSee docs/getting-started.md for a walkthrough.
Two things flip Sky from dev to production: a config block in
sky.toml and a small set of env vars. Both read at process start —
no rebuild needed.
[live]
port = 8000 # default if SKY_LIVE_PORT not set
store = "postgres" # memory | sqlite | redis | postgres | firestore
ttl = "24h"
maxBodyBytes = 5242880 # 5 MiB cap on /_sky/event POST
[log]
format = "json" # plain (dev default) | json (prod default)
level = "info"
[auth]
tokenTtl = "24h"
cookie = "sky_sid"
# tokenSecret read from SKY_AUTH_TOKEN_SECRET (never put secrets in sky.toml)ENV=production # gates dev console + banner OFF; /_sky/metrics behind auth
SKY_LIVE_PORT=8080 # or honour PORT (Cloud Run / Fly / Heroku)
SKY_LIVE_STORE=postgres
DATABASE_URL=postgres://… # fallback when SKY_LIVE_STORE_PATH unset
SKY_AUTH_TOKEN_SECRET=… # ≥32 bytes; Sky errors at startup if shorter
SKY_LOG_FORMAT=json
SKY_LOG_LEVEL=info
SKY_ADMIN_TOKEN=… # /_sky/metrics + /_sky/console require Bearer in prod
# (legacy: SKY_METRICS_TOKEN / SKY_CONSOLE_TOKEN_SECRET still honoured)
# OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318Precedence: process env > .env > sky.toml. The .env file is
auto-loaded but never overrides real env vars — so a docker run -e ENV=production always wins.
The productionFromEnv() gate (ENV then SKY_ENV, anything outside
{dev, development, local} counts as production) governs three things:
dev console mount, 🔍 Console banner, /_sky/metrics auth. One env
var, one switch.
Reference: Sky.Live overview — env precedence.
# easiest path on any system with nix
nix develop # GHC 9.4.8 + Go + system deps, sandboxed
./scripts/build.sh --cleanSee docs/development.md for the full build + test story (pinned toolchain, reproducible Nix builds, contributor guide).
- Core principle: "if it compiles, it works." Every known runtime
panic class has a regression test in
runtime-go/rt/*_test.goortest/Sky/**Spec.hs. Defence in depth (panic recovery +Err- return at Task boundaries) is the floor. - 27 example projects under
examples/— clean build from a wiped slate is a release gate. sky verifyis the canonical runtime check: builds AND runs each example, hits HTTP endpoints, honours per-exampleverify.jsonscenarios.- Test matrix — 306 cabal hspec specs + 25+ runtime Go tests +
70-file
test-files/*.skyself-test loop + format idempotency + Playwright browser sweep across every Sky.Live / Sky.Http.Server example. - FFI generation — Stripe SDK (8 896 types), Firestore, Fyne, and others auto-bind.
Issues + PRs welcome. See the docs tree for architecture context before opening a structural PR.
Apache 2.0 — © 2025–2026 Anzel Lai. Includes patent grant + trademark clause. Prior-art attribution for derivative files (parts of the type-inference core adapted from elm/compiler under BSD-3-Clause) lives in NOTICE.md. Contributions accepted under the same Apache 2.0 terms — see CONTRIBUTING.md.
Sky was previously distributed under the MIT licence (releases up to and including v0.10.0). Those releases remain available under their original MIT terms; v0.10.1 onwards ships under Apache 2.0.