Learn
From nothing to a live graph.
Seven steps. By the end you will have a native binary, a reactive counter, and a page that repaints itself when a row changes in the database — with no fetch call anywhere in your source.
-
Install the compiler
LukeLang builds from source with a C++17 compiler and
make. There is nothing else to fetch for the core language.git clone https://github.com/lucasdmarshall/LukeLang.git cd LukeLang/vm && makeThat produces
vm/build/luke. Full platform notes and the optional dependencies live on the download page. -
Write your first program
Save this as
hello.luke. Files ending in.lukeor.lkuse syntax v2.print("Hello from Luke Build") let name = "Luke" print("My name is " + name)Then build it into a native binary and run it:
./build/luke BUILD hello.luke -o hello ./helloThree ways to run.
BUILDcompiles to native C or WebAssembly with no garbage collector.SHOWbuilds when it can and falls back to the bytecode VM.SHOW … --vmforces the VM, which is the compatibility layer. -
Values and functions
letbinds a value you will not reassign;varbinds one you will. Types are optional where the compiler can infer them and required where it cannot — most often around+, which is both arithmetic and concatenation.let a = 21 var total: float = 0 fn add(x: float, y: float) -> float { return x + y } total = add(a, 21) print(total) // 42Control flow uses braces:
if,while, andreturn. Comparisons and boolean operators are the ones you expect (==,!=,<,&&,||,!). -
Structs, methods and inheritance
A
structis a blueprint: fields with types or initialisers, aninitconstructor, and methods.selfis the instance;superreaches the parent.struct Animal { name: str sound = "…" init(name: str) { self.name = name } fn speak() { print(self.name + " says " + self.sound) } } struct Dog : Animal { sound = "Woof!" fn speak() { super.speak() print(self.name + " wags happily.") } }Instances live in an arena that is released at scope exit — there is no garbage collector on the Build path.
-
Reactive cells
This is where LukeLang stops looking like other languages. A
signalis a cell. Aderivedvalue recomputes when its inputs change. Aneffectruns when the cell it names changes. You never subscribe and never unsubscribe.signal price = 100 signal quantity = 3 derived total = price * quantity effect on total { print("total=" + total) } price = 200 // total=600 batch { price = 120 quantity = 5 // still a single flush }The engine deduplicates work, keeps flush order stable, isolates a failing effect from its neighbours, and disposes cells with their scope. Those guarantees are written down in the reactive specification and enforced by fourteen conformance programs in the test suite.
-
Follow a row to a pixel
The same cell model spans the network. On the server, a cell can be backed by a database row and pushed to a client. On the client, a cell can be backed by that stream and bound to an element.
Server
import std/server import std/sqlite let db = dbOpen("/tmp/luke_live_graph.db") watch user from db where "id = 1" let server = httpListen(8798) let req = httpAccept(server) push watch user on req for 50 beats every 50 msClient
import std/js signal user = "" bind("name", user) watch user from "http://127.0.0.1:8798/watch"Run an
UPDATEagainst that row from any other process and the browser repaints one region. There is no fetch, no cache key, no subscription bookkeeping and no virtual DOM diff in that path — read Live Graph for how the wire and the incremental view maintenance work. -
Ship it
One source, three targets:
# native binary ./build/luke BUILD app.luke -o app # WebAssembly (WASI) ./build/luke BUILD app.luke -target wasm -o app # browser page — html + wasm + runtime glue ./build/luke BUILD app.luke -target browser -o appBrowser and WASI targets need the WASI SDK; see download. When you are ready to put a server behind TLS, deploy covers the reverse proxy and the connection knobs.