Vilan — The Modern Web Language

VILAN

One command, the whole toolchain

The compiler, dev server with hot reload, formatter, test runner, and language server live in one small binary. There is nothing else to install and nothing to configure. Update any time with vilan upgrade.

macOS / Linux

curl -fsSL https://github.com/vilan-lang/vilan/releases/latest/download/install.sh | sh

Windows (PowerShell)

irm https://github.com/vilan-lang/vilan/releases/latest/download/install.ps1 | iex

Homebrew

brew install vilan-lang/vilan/vilan
vilan build
vilan run --watch
vilan fmt
vilan test
vilan-lsp
vilan upgrade

UI that follows your data

A view is a value and a binding is a subscription: bind_text sets the text node once, then sets it again whenever the signal changes. There is no virtual DOM, no render loop, and no dependency array to babysit. Updates land exactly where the data changed.

The snippet is the whole program, and it runs. Try it right here:

clicked 0 times

import std::ui::{ view, mount_root };
import std::reactive::Signal;
fun main() {
let count = Signal::new(0);
let _root = mount_root("app", || {
view("div")
.child(view("p").bind_text(count.map(|n: i32| i"clicked {n} times")))
.child(view("button").text("+1").on("click", || count.set_with(|n| n + 1)))
});
}

the write

count.set(2)
notify

the signal

SignalCell<i32> = 2
re-set

the one text node

<p>clicked 2 times</p>

no virtual DOM, no re-render: the subscription updates exactly one node

The server is a struct. The client is generated.

Mark a method [rpc] and the browser can call it like any other function, typed and checked. Mark a signal [expose] and every connected client holds a live mirror that updates when the server writes. You never write REST endpoints, fetch calls, or the JSON shapes that drift out of sync between them.

notes.vl · one source

[service(NotesClient)]
struct Notes {
[expose] entries: SignalCell<List<Note>>,
}
impl Notes {
[rpc]
fun add(self, text: str): i32 { … }
}
vilan buildvilan build
the servernode
serve_service(4000,
notes.dispatcher() …)

notes.add("ship it")

entries

mirrored live

the clientbrowser
let notes = NotesClient::connect("/rpc");
notes.entries // Signal, live

one definition: the compiler builds both sides and keeps them honest

Services & RPC in the guide
import std::io::print;
import std::option::Option::{ self, Some, None };
fun find_user(id: i32): Option<str> {
if id == 1 { Some("Ada") } else { None }
}
fun greet(name: str): str {
i"hello {name}"
}
fun main() {
print(greet(find_user(2)));
}
Error: Expected str, but got Option<str> instead.
╭─[ demo.vl:10:14 ]
10 │ print(greet(find_user(2)));
│ ──────┬────
│ ╰─────── Expected str, but got Option<str> instead.
────╯

Find out at compile time

Vilan has no null and no exceptions. A value that might be missing is an Option, a call that might fail returns a Result, and the compiler makes you look inside before you use either. The mistake in this snippet is a build error, not a production incident.

Values are copied rather than silently shared, so two names never fight over one object. Most of the mistakes JavaScript saves for runtime cannot even be written.

Option & Result in the reference
app.vl — vilan
1 2 3 4 5 6 7 8 9 10 11
import std::io::print;
import std::option::Option::{ self, Some, None };
fun find_user(id: i32): Option<str> {
if id == 1 { Some("Ada") } else { None }
}
fun greet(name: str): str {
i"hello {name}"
}
fun main() {
print(greet(find_user(2)));
}
⊗ 1vilan-lspLn 10, Col 17 · app.vl
Expected str, but got Option<str> instead.
vilan · live as you type

The editor is in on it

vilan and vilan-lsp ship together so your editor and build never disagree. In-editor diagnostics, hover types and docs, autocompletion, Symbol Rename, formatting, and Organize Imports are all available in VS Code today.

One broken line does not take the tooling down. The rest of the file keeps compiling, serving hovers, and completing while you fix it.

The VS Code extension

Built into the language

No null, no exceptions

A missing value is an Option, a failure is a Result, and match makes you handle both arms. Errors are ordinary values you pass around like any other data.

docs

Values, not references

Assignment copies. Sharing is explicit, borrowing is checked, and spooky action at a distance is a compile error.

docs

Async without the ceremony

await is implicit. Call an async function and the machinery is the compiler's problem. When you want real concurrency, tasks and nurseries give it structure.

docs

One program, two platforms

One workspace compiles the node server and the browser client. The compiler tracks which code needs which platform and keeps each bundle honest.

docs

Rendered before it ships

std::ui renders on the server too: first paint is real markup, then the client rebuilds it live. View source on this page and the content is already there.

docs

A dev loop that keeps up

vilan run . --watch rebuilds in milliseconds and hot-reloads the browser. Format, test, and language server ship in the same binary.

docs

This site is a vilan program: one package, three entries — this page, the playground, and the server that renders both. The server rendered the markup you first saw, and the browser rebuilt it live.

Vilan is built to last. Semantics are settled on paper before they are implemented, and pinned by tests after. A language is a foundation, and a foundation should not move under you.

Read this page's source