Play (
luke SHOW) is a convenience: GC’d bytecode VM for demos and exploration.
Build (luke BUILD) is the language of record: syntax v2 → native code, no GC, Rust-class lightness.
This is Path A: Python-shaped versatility on the surface, Zig/Rust-shaped cost underneath.
Commands
luke SHOW examples/build/hello.luke # prefers Build (native temp); --vm forces Playluke BUILD examples/build/hello.luke # Build — native binary, no GCluke BUILD examples/build/hello.luke -o helloluke BUILD examples/build/hello_wasm.luke -target wasm -o hello.wasmluke BUILD examples/build/hello_browser.luke -target browser -o hello_webluke BUILD examples/build/functions.luke -target debug -o fn # -O0 -g for gdbluke DEBUG examples/build/functions.luke --break 10 # interactive gdb (.luke:line)luke DEBUG examples/build/functions.luke --break 10 --batch # CI: break + next/step/finishluke LSP # stdio LSP: hover/outline/FMT/refs/rename/…# run wasm (WASI): node scripts/run_wasi.cjs hello.wasm# run browser wasm headless: node scripts/luke_browser_loader.cjs hello_web.wasm# or open hello_web.html in a browser
Build emits C with #line N "file.lk" per statement, then compiles with the system C compiler
(cc -O2 -g native, or -O0 -g -fno-inline for -target debug / luke DEBUG). WASI SDK for
-target wasm|browser. Debugger skips luke_rt.h / stdlib headers so next = step over,
step = step into Luke FUNCTION, finish = step out. luke DEBUG --inspect dumps reactive
cell values and dependency edges (luke_rx_inspect_cstr). luke DAP is a stdio Debug Adapter
Protocol server (gdb backend) for editors — Reactive scope shows named cells + deps.
Browser also writes *.html + luke_browser_loader.js (copied beside the wasm) for <script> tags.
IMPORT + stdlib + packages
import "./critter.luke" # relative moduleimport std/files # read/write TEXT filesimport std/json # JSON tree helpersimport std/http # httpGet (native)import luke/greeter # package from luke_modules/greeter
std/* resolves from vm/stdlib/. Relative paths are next to the entry file.
Packages (luke/<name>)
Look up order:
1. luke_modules/ next to the source file
2. ./luke_modules, LUKE_PACKAGES (colon-separated roots)
3. Package dir must contain luke.pkg (with entry=…), or main.luke, or <name>.luke
luke_modules/greeter/luke.pkg # entry=main.lukeluke_modules/greeter/main.luke
IMPORT package:greeter is an alias for IMPORT luke/greeter.
Foreign FFI imports (C/JS/Python bridges) are intentionally not magic IMPORT numpy — parked for later; C wrappers will come first.
| Module | Helpers |
|---|---|
std/files |
readFile, writeFile, fileExists |
std/json |
jsonParse, jsonGet, jsonIndex, jsonLen, jsonHas, jsonAsText / Number / Flag, jsonStringify, jsonString |
std/http |
httpGet (native via curl; empty on WASI) |
std/server |
httpListen, httpAccept, httpReply, httpPath / Method / Query / Body, httpMatch, httpQueryMap, httpHeader / Cookie / SetCookie, httpJson, SSE helpers |
std/sqlite |
dbOpen/dbClose thread-local pool, stmt cache, WAL; dbExecBind / dbQueryBind; auto -lsqlite3 |
std/pg |
pgOpen/pgClose/pgCheckout/pgCheckin, pgQueryBind/pgExecBind/pgRowsBind — Slipstream default (LUKE_PG_ASYNC=0 → blocking); auto -lpq |
std/auth |
authInit, authCreateAccount, authLogin / Logout, authRequire, authCsrf / authCheckCsrf (links -lsodium; set LUKE_AUTH_SECURE=1 behind HTTPS) |
std/args |
argCount, getArg |
std/env |
getEnv, setEnv |
std/paths |
cwd, pathJoin, pathBasename, pathDirname |
std/process |
shell, exitWith |
std/js |
jsSetText, jsSetHtml, jsGetValue, jsFetch, jsOnClick, jsLoadFont, jsAddStyle, jsSetTitle |
luke/… |
Your packages under luke_modules/ (luke PKG init <name>) |
Browser page ownership (conversational)
page.title("LukeLang")page.font("Syne", "./fonts/syne-700.woff2" # local pack → @font-face + copy)page.style(""" body { font-family: Syne, sans-serif; }""")fill("root", """ <h1>LukeLang</h1> <button id="go">Go</button> <p id="out"></p>""")raw """WHEN "go" IS CLICKED DO FILL "out" WITH "Still LukeLang."END WHEN"""
-target browser emits HTML with Luke title/CSS/body/fonts baked in, wasm beside it, and inlines vm/runtime/luke_browser_boot.js (runtime — not app JS). Dist has no luke_browser_loader.js.
See sample/landing.luke.
Collections + problems (conversational)
var nums: list = []nums.push("one")print(nums[0])print(nums.len())var bag: map = {}bag["name"] = "Luke"print(bag["name"])try { throw "could not finish"} catch (problem) { print(problem)}
TEST
test "math" { assert 1 + 1 == 2}
luke TEST examples/build/collections_test.luke
Packages
luke PKG init mylibluke PKG install echoluke PKG publish mylibluke PKG lock # writes luke.lock
Arena scopes
raw "IN ARENA DO"let tmp: str = "ephemeral"print(tmp)raw "END ARENA"
Bump pointer is restored at END ARENA — request/frame-scoped memory without GC.
Guarantees (Build)
| Rule | Meaning |
|---|---|
| No GC | Memory comes from stack, statics, or a bump arena freed in bulk |
| Fixed layouts | BLUEPRINT fields are a C struct — not open hash maps |
| Known types | Locals/fields are NUMBER, FLAG, TEXT, or a blueprint type |
| Native code | Ahead-of-time C → machine code (and later WASM) |
| Same voice | SPEAK, MY NAME IS, ASK, BLUEPRINT still work |
Types (Luke words)
| Luke | Build representation |
|---|---|
NUMBER |
double |
INTEGER |
int64_t (exact IDs / money cents / counters) |
FLAG |
int (0/1) |
TEXT |
LukeText { ptr, len } (arena or literal) |
JSON |
LukeJson * (arena tree — parse / get / stringify) |
LIST |
LukeList * (arena-backed text items) |
MAP |
LukeMap * (arena-backed text keys/values) |
SERVER / REQUEST |
HTTP server / request handles |
DATABASE |
SQLite handle |
BLUEPRINT Foo |
typedef struct Foo { ... } Foo |
Inference (v0):
- 42 → INTEGER (no . / exponent); 3.14 → NUMBER
- "hi" / wordy strings → TEXT
- TRUE / FALSE → FLAG
- ASK jsonParse WITH … → JSON
- HAS name SET TO "..." → field TEXT
- HAS count SET TO 0 → field INTEGER
- HAS x AS NUMBER / AS INTEGER / AS TEXT / AS FLAG / AS JSON — explicit when needed
- First assignment to a local fixes its type; later SET must match (INTEGER widens to NUMBER)
- Function args/arity and GIVE BACK types are checked
- Optional: THIS IS FUNCTION f … GIVES BACK TEXT DO
- Concurrent HTTP: ASK httpServe WITH server, handler, maxConn (SO_REUSEPORT multi-loop + handler pools; links -lpthread)
- Routing / request shape: httpMatch, httpQueryMap, httpHeader / httpCookie / httpSetCookie, httpJson
- Parameterized SQL: dbExecBind / dbQueryBind (prefer over string-concat dbExec / dbQuery)
- Auth: IMPORT std/auth — Argon2id passwords (libsodium), REQUIRE LOGIN, THE CURRENT USER, CSRF; see BACKEND_ROADMAP.md
- SSE: httpSseOpen / httpSseData / httpSseId / httpSseComment; httpLastEventId for resume
- INTEGER rules: see INTEGER.md — checked overflow, DIVIDE→NUMBER, widening vs truncating conversion
- Backend track: BACKEND_ROADMAP.md · master checklist TaskList.md
Backend concurrency ceiling
httpServe is N SO_REUSEPORT event loops (default: one per CPU) plus per-loop handler pools:
- Event-loop I/O —
epoll/kqueue/poll; non-blockingaccept4, read,writev. Edge-triggered on Linux (EPOLLET). Idle / half-open clients do not occupy a thread. - Multi-core accept — each loop owns its listen socket with
SO_REUSEPORTso the kernel load-balances connections (nginx / Go-netpoller model).LUKE_HTTP_LOOPSoverrides the count. - Handler pool — workers run only after a complete request is buffered. Pools are per loop (no global enqueue mutex across cores).
LUKE_HTTP_INLINE=1runs the handler on the loop thread (REST bench / run-to-completion). - Per-request cost — pooled arenas (
luke_arena_clear, default first block 8 KiB), embeddedLukeHttpServeJobon the connection,TCP_NODELAYon accept. - HTTP/1.1 keep-alive — default
LUKE_HTTP_KEEPALIVE_MAX=100000(env0= unlimited). - Timeouts — idle
READ/WRITEsweep (LUKE_HTTP_TIMEOUT_MS). - Graceful stop —
SIGTERM/SIGINTstop accepts, close idle sockets, drain in-flight handlers. - Chunked / SSE — leave the loop; may block one worker for the stream lifetime.
- Escape hatches —
LUKE_HTTP_IO=pool(legacy blocking recv); seeDEPLOY.md.
Positive maxConn is a lifetime accept budget across all loops; maxConn ≤ 0 accepts until signal/failure.
Memory (Luke words)
| Word | Meaning in Build |
|---|---|
| (default locals) | Stack / registers |
NEW instances |
Allocated in the thread arena (bump); live until arena reset/program end |
TEXT concat / dynamic strings |
Arena-backed |
IN ARENA … END ARENA |
Checkpoint + reset bump pointer (scoped bulk free) |
DROP (roadmap) |
Early release of a single value |
There is no mark-sweep collector in Build binaries. Peak memory is predictable: stack + arena high-water mark.
Blueprints
struct Dog : Animal { sound = "Woof!" init(name) { self.name = name } fn speak() { print(self.name + " says " + self.sound) }}
Lowers to roughly:
typedef struct Dog { Animal base; /* or flattened parent fields */ LukeText sound;} Dog;void Dog_speak(Dog *self);Dog *Dog_born(LukeArena *a, LukeText name);
ASK buddy TO speak→Dog_speak(buddy)(static dispatch when type known)CALL PARENT speak→Animal_speak((Animal*)self)PRIVATEfields → C name mangling; only methods of that blueprint may touch themALWAYS HAS→ static/globals on the blueprint’s C file scope
What Play allows that Build may reject
- Adding undeclared fields at runtime
- Truly dynamic
ASKon unknown types (Build wants a known blueprint type) - Unlimited runtime mutation of object shapes
- Relying on GC to clean cycles (Build uses arenas — don’t build immortal graphs by accident)
Play remains for sketching. Shipping artifacts should BUILD.
Roadmap toward “Python everywhere + Rust light”
- ~~Build → C + arena runtime~~
- ~~Clearer Luke-voice Build errors;
AS TYPEannotations~~ - ~~Packages / relative
IMPORT+std/files+std/json~~ - ~~
luke BUILD -target wasm(WASI)~~ - ~~Richer typechecking; fuller JSON; thin HTTP GET~~
- ~~Browser-oriented WASM packaging (
-target browser)~~ - ~~Package registry (
IMPORT luke/<name>+luke_modules/)~~ - ~~SHOW prefers Build; Play VM is the compatibility layer (
--vm)~~ - ~~Tooling stdlib (args/env/paths/process) +
luke PKG init~~ - ~~Browser JS bridge (
std/js)~~ - ~~
IN ARENA/END ARENAscopes~~ - ~~Remote package registry (
luke PKG install+registry/index.json)~~ - ~~Foreign imports (
IMPORT c:+FOREIGN FUNCTION)~~ - ~~Build IR shared frontend (expand/soften for Play;
luke IRdump)~~ - ~~LIST / MAP + ATTEMPT / OTHERWISE +
luke TEST~~ - ~~
std/server+std/sqlite+ browser fetch/click~~ - ~~
luke PKG publish+luke.lock~~ - ~~
luke LSPstdio diagnostics beachhead~~ - Richer remote registry (signed packages)
- Explicit Python bridges (beyond C FFI)
- Optional: emit Play bytecode opcodes directly from Build IR nodes
Rendering / layout (engine track)
- Argus (rendering):
ARGUS.md— scene paint → DOM presentment. - Hanka (layout):
HANKA.md—COLUMN/ROW/STACK→ frames → Argus. - Production web:
PRODUCTION_WEB.md— inputs, routes, deploy checklist. - Reactive (Phases 1–8):
REACTIVE.md— full reactive stack shipped.
import std/hankaimport std/argusraw "BEGIN COLUMN AT 48, 420 SIZE 1184, 280 PAD 0 GAP 16"raw "SLOT TEXT \"brand\" SIZE 900, 80 SAY \"LukeLang\""raw "END COLUMN"raw "LAY OUT THE SCREEN"raw "PAINT THE SCREEN"
luke BUILD examples/build/hanka_demo.luke -target browser -o build/hanka_demo
Philosophy
Conversational syntax is the UI.
Build-mode layouts, types, and arenas are the truth.
That split is how Luke can feel like Python and weigh like Rust.
Parser ceiling (known limit)
Build codegen (vm/src/build_c.cpp) is line-based: one statement per line, startsWithCI / keyword scans, not a full lexer + AST.
Syntax v2 parses to an AST and lowers to that line-based surface, so the ceiling still applies underneath: multi-line expressions, richer nesting, and flexible punctuation will need codegen to consume the AST directly.
| Today | Ceiling |
|---|---|
Prefix/findOutsideQuotes stmt match |
Awkward multi-line constructs |
| One line ≈ one stmt | Soft line-wrap / continued statements |
Hand-rolled expression splits (AND, ADD, …) |
Precedence tables / proper AST |
Roadmap: expression parentheses + paren-aware op scan shipped (stripOuterParens, luke_ast.hpp IR stub). Next: tokenize → Pratt expr AST → lower into Build codegen; then statement AST so LSP/formatter share one tree. Do not reintroduce a parallel JS parser.