Releases: vilan-lang/vilan
Release list
vilan 0.15.0
Module bindings initialize in dependency order. A top-level let now runs after every binding its initializer actually evaluates — the ones it reads, plus everything read inside whatever it calls on the way — so a binding may reference one declared below it, in the same file or in another module, exactly as a function may call one declared later. Creating a closure evaluates nothing, so two module-level closures may still name each other freely. This kills a real miscompile: declaration order used to follow the order names happened to be listed in your imports, so a constant that depended on another could be emitted before it and crash at load with Cannot access 'X' before initialization — with nothing at compile time to warn you. v0.12.0 made the emitted JavaScript independent of import statement order; this closes the other half, the names inside a { … } brace set, so no spelling of your imports can change what your program does or the bytes it compiles to — vilan fmt can sort them freely. And a genuine cycle among initializers (including a binding that reads itself) is now a compile error that names the round trip (via A → B → A), anchored at the read that closes it and noting each participant's declaration, instead of a crash at load. The order is specified rather than incidental: spec §7.1 fixes dependency order first, then a canonical module order — the standard library first, then dependency packages, then your own, modules within a package by name, the entry file last — for bindings that depend on nothing from each other. One behavior note: a module initializer with side effects may now run in a different relative order than before — the old order was whatever your import listing happened to produce; the new one is the rule above. Bindings that actually depend on one another are unaffected: those were the broken case.
Git dependencies. A dependency can now come from a repository, pinned to an immutable point: shapes = { git = "https://…", tag = "v1.2.0" } (or rev = "<commit sha>" — exactly one; a branch is refused, because a branch moves and so cannot pin anything). The checkout is fetched shallowly, verified to be a vilan [library], and cached content-addressed under ~/.vilan/ — after the first fetch, builds are fully offline, and the cache serves the pinned content even if the upstream moves or disappears. A dependency's own git dependencies resolve through the same cache. Fetching happens only when a build of a declaring project needs it — the toolchain still makes no passive network calls, and the editor never fetches at all: a not-yet-fetched dependency shows as a note to run vilan build. Workspaces got a matching quality-of-life: [project.dependencies] declares a dependency once at the workspace root, and a member opts in with shapes = { project = true } — explicit, so nothing is inherited by surprise.
vilan.toml speaks in the editor. The manifest now has completions — keys per table, values where they're enumerable (targets, presets), quotes placed for you — and its problems finally surface where you're looking: a manifest that doesn't parse, an invalid dependency, or a git dependency that isn't fetched yet publishes a diagnostic on vilan.toml itself and clears when you fix it.
Installing vilan is becoming one command. The Homebrew tap is live today: brew install vilan-lang/vilan/vilan (macOS and Linux, both architectures). The npm package (@vilan-lang/vilan — the command is still just vilan) and the VS Code Marketplace / Open VSX listings are built and ship with the next releases as their publishing credentials land. vilan upgrade learned to respect whoever installed it: an npm- or brew-managed vilan is steered to npm update -g / brew upgrade instead of overwriting files the package manager owns — the curl-script install keeps upgrading itself exactly as before.
A save during the first watch build is never lost. vilan run --watch took its file-change baseline after the initial build, so a save landing while that build ran was silently absorbed — the watcher never noticed, and your change sat unbuilt until you saved again. The baseline now precedes the build: a save at any moment after the watcher starts triggers a round. Found because a CI test kept "flaking" — it was right, four times, on three platforms.
vilan 0.14.0
Vilan runs on Windows. Native, not WSL: install with one PowerShell line (irm https://github.com/ReedSyllas/vilan/releases/latest/download/install.ps1 | iex), and the whole toolchain is there — vilan.exe and vilan-lsp.exe, the compiler, run --watch with hot reload, fmt, test, and vilan upgrade (which learned the Windows swap: a running executable can't be replaced in place, so the old one steps aside and is swept on the next run). The VS Code extension finds the server on Windows now, and the language server treats every spelling of a file — C: vs c%3A, even DOS-era RUNNER~1 short names — as the one file it is, so diagnostics never duplicate or stick. Stopping a watch round kills the whole process tree (a forking dev server can't hold its port hostage), colors render in both Windows Terminal and classic conhost, and the entire test suite now runs green on Windows in CI as a required check on every change — this isn't a port that will quietly rot.
Line endings became law. A \r\n in source is one line terminator, and string literal values are built from the normalized text — a multi-line string contains \n regardless of how your editor or Git saved the file. This closes a real miscompile: the same program checked out with CRLF endings used to embed \r in its string values and emit different JavaScript than its LF twin. A leading byte-order mark is now stripped everywhere source is read, canonical Vilan is LF (vilan fmt converts), and a .gitattributes keeps every checkout byte-stable. The full corpus compiles byte-identically from LF, CRLF, and BOM'd copies — pinned, on both platforms.
Module names are case-exact — everywhere. New language rule (spec §4.2): an import must match the on-disk file name byte for byte. On a case-insensitive filesystem, import foo finding Foo.vl is now a clean diagnostic naming both spellings instead of a resolution — so a program that builds on Windows builds identically on Linux, with no case-sensitivity surprises waiting in CI.
Errors print to stderr now, and every diagnostic respects NO_COLOR. Compile errors joined warnings on stderr — vilan build --stdout can never again interleave a diagnostic into the JavaScript it pipes — and the ariadne-rendered reports finally obey the same terminal gate as the rest of the CLI: colored when you're looking, byte-plain when piped or NO_COLOR is set. If you were parsing errors from stdout in a script, read stderr instead.
vilan 0.13.0
Server-side rendering: render and replace. A full-stack app can now serve its first paint as real HTML — for the crawler, and for the human who sees content before any JavaScript arrives. The model is deliberately simple: on the server, import std::ui resolves to a render-only implementation of the same API, so the same component functions build an HTML string instead of DOM — each bind_* embeds its signal's current value, event handlers are accepted and ignored — and render(view) hands your route handler the markup to splice into its shell. On the client, mount now replaces the container's contents when it boots, so the server HTML gives way to the live, bound tree in place. There is no hydration — no node adoption, no mismatch errors, no second set of rules — and that is a design decision, not a gap: the eventual path is resumability, which makes hydration's machinery obsolete anyway. The two ui implementations are held together by a differential test that renders one shared component through both and requires byte-for-byte agreement. The new examples/ssr is the working loop, and the SSR guide explains the one rule that matters (build pure, bind reactive) and where v1 fits: self-contained and server-data-seeded apps — views that read a live rpc client while building are client-side by nature.
Snippets in completion. for, fun, struct, and match now complete as tab-through templates alongside their bare keywords — parameter names, field stubs, and match arms pre-placed — degrading to plain keywords in editors without snippet support.
The CLI dresses for the terminal. Build, watch, test, fmt, and upgrade output is colored when you're looking at it — green successes, bold red errors, cyan dev-loop lines — and byte-for-byte plain the moment it's piped or NO_COLOR is set.
vilan 0.12.0
The editor grew up. Completing a function now inserts a real call — tab-through parameter placeholders by default (greet(name, times)), parens-only or plain-name via the new vilan.completion.functionCall setting — with the full signature and /// doc shown right in the suggestion popup and parameter hints opening as you land in the parens (completing a callee you already parenthesized, or a function passed as a value, stays bare). Hover now answers everywhere: variables show their typed binding, parameters show their declared convention (own x: T, x: &mut T), and every keyword explains itself in one line with a deep link into the book. Organize Imports sorts and prunes: unused imports and brace-set branches are removed conservatively (never while the file has errors, never re-exports, never an import that only a derive's generated code uses — the compiler knows), with an opt-in vilan.organizeImports.onSave. Inlay hints and semantic tokens gained toggles, every setting applies live, and — pinned by a thirteen-test guarantee — the language server keeps working in files with errors: hover, navigation, completion, and the outline all serve the parsed remainder on both sides of a typo.
vilan fmt sorts imports, and import order stopped mattering. Top-level imports format into one canonical order (std first, then dependencies, then pkg; brace sets alphabetized; comments travel with their line; block-scoped imports deliberately untouched) — and underneath it, the compiler now walks modules in a canonical order too, so the emitted JavaScript is byte-identical no matter how your imports are arranged. Reordering an import can never again churn your build output.
The extension ships its licenses (MIT OR Apache-2.0) in the package, ready for the marketplace.
vilan 0.11.0
Hot module replacement — the dev loop closes. vilan run --watch on a full-stack workspace now hot-reloads the browser: save a file and the app updates in place with module-level state carried across the swap (plain values by value, Signal/Shared by payload into fresh cells — keyed and fingerprinted by the compiler, so a changed shape fresh-inits instead of adopting stale data), while the server leg restarts behind the scenes and the client's rpc mirrors resync on their own. A CSS-only edit hot-swaps the stylesheet without a reload; a compile error shows an in-page overlay carrying the actual compiler diagnostics (file, line, message, note — the terminal's own rendering) and clears on the next good save; std::dev gives app code on_teardown and a type-checked stash/take carryover (only plain data may cross a swap — the compiler enforces what Vite leaves to convention). Watch rounds got structurally cheaper too: parse results are content-cached across rounds and a leg whose sources re-hash identically is skipped outright, its artifacts reused byte-for-byte. Multi-server workspaces pick their entry with vilan run --entry <name>. The dev-loop guide walks the whole loop.
The frontend is handwritten now — builds are ~2.7× faster. The chumsky combinator frontend is gone, replaced by a hand-rolled lexer and recursive-descent parser proven byte-identical first (279/279 whole-file tree agreement, every corpus program compiled to identical output through the new code before it was wired in) and then measured: a release build of the todo client dropped from ~0.49 s to 0.18 s, instruction counts fell 5.21 B → 2.01 B, and the frontend went from ~63% of a compile to under 4% — the debug binary gains the most. Parse errors improved with it: the 30-token "expected one of …" dumps are gone, a missing separator reports found 'y' expected ',' or '}' at the offending token, the a!==b spacing trap gets a first-class hint, and a syntax error no longer discards the whole file — the parsed prefix survives, so the language server keeps working on everything above the typo.
Trait impls must now match their trait's signatures. Previously an impl satisfied a trait by member name alone; receiver convention, parameter types, arity, and return type were never compared, so a wrong fun drop(self) compiled against fun drop(&mut self). Every member is now checked under the trait's own generics (Self included), with the mismatch spelled per dimension. This can reject code that previously compiled — the fix is to make the impl say what the trait says. (A deliberate leniency: an async impl of a sync-declared method stays legal — dispatch is monomorphized, so the caller always knows the concrete callee.)
Two real bugs died. A module-level closure referenced only by calls (let helper = || …; used as helper()) was tree-shaken out of the bundle while its call sites remained — a runtime ReferenceError; calls now count as references, and six sibling shapes (calls through ?./!, transitive closure chains, nested modules) were quietly broken the same way and are fixed with it. And a typo'd name in value position no longer cascades — one unknown identifier is one error, not a fan of Expected i32, but got void noise at every use.
vilan fmt formats everything. The formatter silently returned files unchanged when they used newer constructs — destructuring, fixed arrays, ?. chains, the macro forms, numeric suffixes. Every construct now has its printer, guarded by a standing zero-bail gate over the whole corpus, and two latent printer bugs found on the way (one would have reformatted -(2 + 3) into -2 + 3) are fixed. The standard library itself is freshly vilan fmt-formatted.
Sharper diagnostics across the board. Notes that pointed into std for user-caused conditions were audited (they are all genuinely declaration notes — "the trait declares it here" — and stay); one unresolved name suppresses its whole echo family; and the diagnostics ledger now runs as a living gate — every new compiler error message gets verdicted against the standard as it lands, not in batches after the fact.
vilan 0.10.0
Resources: values that clean up after themselves. A resource struct (or resource external struct) is the new owned-resource class — a value with exactly one owner that moves on binding and own-passing instead of copying, is loaned through the ordinary view conventions, and runs its Drop at its owner's scope end, every exit included (ret, jump, panic unwinding — and a value-returning main now runs its drops before the process exits). Containment infers: a struct, enum, tuple, or fixed array holding a resource is one. Option.take/replace are the sanctioned partial move, std's drop(value) destroys early with no public close() anywhere, and the affine checker rejects the whole double-close family at compile time — use-after-move (with a note at the move), conditional moves, moves in loops, resource captures in closures and spawns, resources in native containers, coercions to any, and derives (Wire/Hashable/PartialEq) on resource-holding types. Database is the first real resource: it closes its node:sqlite handle deterministically, module-level handles keep process lifetime (the serve-forever idiom — now loan-only, and reachable from closures, which the checker previously miscounted as captures), and OwnedNursery owns background tasks whose real failures still reach the console with their spawn origin while cancellation echoes stay silent. The resources tour walks it; spec §6.8 is the contract.
One law now opens the memory model. Spec §6.0: every alias is a claim on an owner whose epoch advances on a fixed set of events — and a claim is valid while its owner's epoch is unchanged. Views are the statically-proven claims, handles the dynamically-checked ones, and every mechanism in the chapter (views, projections, Arena/Handle, Shared, resources) is presented as a cell in that one table.
Rule 4 is now enforced everywhere views actually come from — and it's smarter about what invalidates. Previously only a direct &place view was policed; a view returned through a call (list.at(0), arena.get(h)) or bound by a Some(let v) match capture was invisible to the invalidation checks (and a chained projection didn't even lower as a view — a real miscompile, fixed). Now every view anchors at what it projects, multi-parameter projections anchor at all of them, and mutating a viewed container, reassigning its root, or holding any of these across await is the same compile error the direct form always raised. This can reject code that previously compiled — re-derive the view after the mutation or suspension, as ever. In exchange, the checker stopped over-rejecting: only calls that may change a container's geometry (grow, shrink, reallocate, swap an aggregate field — inferred per method as the new bumps effect, hover-visible beside borrows) conflict with a live view; a method that merely writes fields or elements through &mut self now passes freely.
Arena.get hands back a live view — Option<&T> borrows self, the shape the spec always described, instead of a copy; set remains the write path, and stale handles still answer None.
vilan 0.9.0
Higher-order functions adapt to async callbacks. map is one function, not two: passing an async closure instantiates an async copy of the receiving function — its calls through the parameter are awaited, sequentially (each callback settles before the next begins) — while every sync call site keeps the untouched original. Adaptation follows the closure through plain parameters transitively (helper(xs, f) forwarding into map adapts end-to-end), an adapting function traverses a snapshot of its receiver so interleaved work can't tear the iteration, and it stops honestly at the boundaries: a parameter marked sync declares the synchronous contract (the reactive graph's recompute positions — Signal::map, turn, batch, the UI render callbacks — are sync), host (external) functions can't await your closure (unless a parameter is declared async |…| T — the typed channel), and trait/generic dispatch has no static callee to instantiate. When the elements are independent, opt into concurrency with the spawn-then-settle idiom: .map(|x| async work(x)) then Task::settle_all(tasks).
Spawning grew a spine: Task<T>, and nurseries to own them. async expr now yields a Task<T> — an eager, opaque handle; copying it refers to the same task. Every task absorbs its own failure at construction: a spawned panic can never crash the program as a host "unhandled rejection" — a later await receives it, and a task nobody observes reports the error to the console stamped with the function that spawned it, then execution continues. Task::settle_all joins many; Task::race yields the first to settle. Raw host promises stay Promise<T> at the extern seam, and await unwraps both.
nursery(body) is structured concurrency (std::task): every task spawned in the body's dynamic extent — by the body, by anything it calls, by the tasks themselves — is joined before the nursery returns the body's value. Failures follow the first-observed rule: a body throw wins, otherwise the earliest-settled task failure, re-raised from the nursery call with its spawn origin while every other task is absorbed. n.cancel() aborts the whole extent — the nursery's AbortSignal rides ambiently into sleep and fetch, so cancellation cuts in-flight IO short instead of waiting it out (a live e2e cancels a fetch against a hanging endpoint and joins in ~3s instead of 60), cancellation rejections are absorbed echoes rather than errors, nurseries chain so an outer cancel reaches nested extents, and Task::race + n.cancel() is the race idiom. The first real failure cancels the same way, so one task's error stops its siblings' work at settle time — not when the join happens to look. Spec §7.7 is the contract; the async tour walks it.
Asyncness now rides every value channel. async |T| U is accepted on struct fields and function return types (calls through a field read or a returned closure await implicitly), unannotated bindings adopt asyncness from any value they hold — including mut rebinds — and storing an async closure where a plain value-returning closure type is declared (a field, a return type) is a compile error instead of a promise wearing the wrong type. Void-returning positions keep spawn semantics, which is why UI handlers await freely with no ceremony. The standard library's own transport and draft plumbing was migrated off its workarounds in the process.
Variadic tuple bounds are enforced. T: (2..) and (..: Display) parsed since variadics landed but checked nothing; arity ranges and per-element trait bounds now hold at every call and construction site, with the note pointing at where the bound was declared.
Editor and diagnostics tail. Notes can point into another file (the "declared here" half of a cross-module error lands in the right source); inlay type hints for inferred let bindings; semantic tokens gained modifiers; parse errors name the split (a! == b vs a != b) instead of dumping the expected-token soup; x.field() on a closure-valued field steers to (x.field)(); and multi-file diagnostic publishing dedupes across dependents, so fixing a shared module clears its dependents' stale squiggles in one pass.
vilan 0.8.0
Diagnostics got a standard — and every one of the compiler's 180 diagnostics was audited against it. The rules: anchor at the narrowest span that identifies the problem, in code you wrote; speak your vocabulary; name the fix when it's unambiguous; and never bury a root cause under its own consequences. What the audit shipped: "cannot find" errors now steer to the import when the name uniquely belongs to a module (cannot find type 'JsonValue' — import it first (\import std::json::JsonValue;`)`); a conflict with an inferred type points at where the inference happened — the closure's first call, the variable's initializer — as a second label at that exact spot; "has no method" anchors at the method name instead of the argument list; an error inside macro-generated code anchors at the attribute that generated it, in your file; and the near-empty "could not be resolved" residuals only appear when they're the lone signal instead of trailing a real error.
/// is the doc-comment syntax. Hover surfaces /// blocks; a plain // comment is an implementation note and stays private. The standard library is documented with it — hovering now(), format, or any std function shows its docs in the editor.
The editor understands the code, not just the text. Semantic highlighting comes from the analyzer: a generic parameter at its use site, a macro name sharing a trait's name, a method call versus a field read, a module qualifier — each colors by what it is. Hover on a constant shows its evaluated value (SIZE: i32 = 64), signatures render their context clauses, and [ before an item completes the registered macro names, derives included. Unsaved edits were already visible to dependent files as of v0.7.0; the editor now reads as precisely as it recompiles.
vilan 0.7.0
Expression lifting: a bare ? lifts the whole expression. Where ?.
continues a member chain, ? on its own lifts the rest of the surrounding
expression — count? * 2, deadline? < now(), and the two-receiver form
price? + tax?, which is good only when every receiver is and
short-circuits left to right (a receiver after a None/Err never runs,
like &&; on Result, the first error wins and every receiver shares one
error type). The lift stops at natural boundaries — call arguments, struct
fields, parentheses — and a ? that lifts nothing, or would turn an if
condition into an Option<bool>, is an error with a steer. ?. chains are
unchanged. Option/Result only for now; lowers to plain branches, no
closures.
Fixed arrays round out: .len() and destructuring. arr.len() folds to
the constant (the length lives in the type; a side-effectful subject still
evaluates, exactly once). let [r, g, b] = rgb; destructures — irrefutable,
element count checked against the type, nesting arrays and tuples freely,
and it works in parameter position (fun sum([a, b]: [i32; 2])). Elements
come out as value copies, like everything else.
Conditions are type-checked now. if 5 { .. } used to compile and
branch on JS truthiness — an Option condition always took the branch.
Every if/for condition must now be a bool, spanned at the condition.
Two soundness holes closed. An unannotated Map::new() never grounded
its key/value types, so mixed-typed inserts compiled and ran — a binding
whose type keeps a callee's parameters now demands an annotation. And a
derive's internal imports leaked into the deriving module (JsonValue
resolved with no import after [derive(Json)]) — expansion imports are
scoped to the expansion now.
Editor and diagnostics. Unsaved edits propagate to dependent files
immediately (analysis reads open buffers, not disk). A conflicting call on
an unannotated closure names the first call that fixed the parameter's
type. A heterogeneous list literal ([1, "x"]) is rejected instead of
silently typing by its first element.
vilan 0.6.2
Two generic miscompiles fixed. A &mut T view resolving to bool through
a generic, and integer division / bitwise ops on i32/u32 through a generic,
silently did the wrong thing: the boolean write-through was a no-op, i32/u32
division skipped its truncation (7 / 2 came out 3.5), and a u32 shift used
the signed operator. Both were monomorphization-time classifications that dropped
their verdict for the native-JS types — concrete code and every other integer
width were already correct. Found by an audit after v0.6.1's &mut bool fix.
! guides you to convert errors. ! returns a failure as-is, so the error
types must match; when they don't, the compiler now points at the fix instead of
calling it unsupported — .map_err(…) to change a Result's error, .ok_or(err)
to turn an Option's None into one. Conversion stays explicit (no hidden
From behind the operator), by design.