個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證正在載入完整 README。 以下為已儲存的摘錄。
A scripting-feel, structurally-typed language with types hidden by aggressive
inference, compiling to lean WebAssembly (WasmGC). Ships an LSP-backed editor
experience, a CLI, and an in-browser playground. See ROADMAP.md for
the plan and DECISIONS.md for the rationale; design notes live in
docs/.
VL is self-hosted. The compiler is written in VL (compiler/*.vl),
compiled to a WebAssembly seed (build/vl-compiler.wasm), and run through a thin
Rust host (scripts/vl-host) — an OS/wasmtime shim that never
parses or types VL itself. See docs/genesis-design.md
for how the seed is bootstrapped.檢視完整 README ↗A scripting-feel, structurally-typed language with types hidden by aggressive
inference, compiling to lean WebAssembly (WasmGC). Ships an LSP-backed editor
experience, a CLI, and an in-browser playground. See ROADMAP.md for
the plan and DECISIONS.md for the rationale; design notes live in
docs/.
VL is self-hosted. The compiler is written in VL (compiler/*.vl),
compiled to a WebAssembly seed (build/vl-compiler.wasm), and run through a thin
Rust host (scripts/vl-host) — an OS/wasmtime shim that never
parses or types VL itself. See docs/genesis-design.md
for how the seed is bootstrapped.
You need Rust ≥ 1.96 (for the host — the pinned wasmtime requires it) and a
compiler seed. For a sandboxed,
batteries-included environment (node + deno + rust + Claude behind a network
firewall, safe for claude --dangerously-skip-permissions), see the
Dev Container.
# 1. Build the native `vl` host (~once)
cargo build --release --manifest-path scripts/vl-host/Cargo.toml
# 2. Get a compiler seed into build/vl-compiler.wasm
scripts/fetch-seed.sh # download the published compiler seed
# …or, from a source checkout with an existing seed:
scripts/refresh-compiler.sh # self-compile compiler/*.vl with the seed (~40s)
# 3. Run a program
echo 'print(6 * 7)' > hello.vl
scripts/vl-host/target/release/vl run hello.vl
Put the host on your PATH for convenience:
export PATH="$PWD/scripts/vl-host/target/release:$PATH"
vl run hello.vl
vl toolusage: vl <build|check|run|fmt> <file.vl> [-o out.wasm] [-O|-O3] [-w|--check] [--compiler vl-compiler.wasm]
std and the compiler seed both ship INSIDE the binary, and every on-disk copy is a DEVELOPMENT override that is explicit and announced. Resolution, first hit wins:
| seed | std | |
|---|---|---|
| explicit | --compiler <wasm> | — |
| explicit, announced | $VL_COMPILER_WASM | $VL_STD |
| a development tree | ./build/vl-compiler.wasm | <tree>/std |
| always present | the embedded seed | the embedded std |
A development tree is a checkout holding compiler/entry.vl and a real std/, found by
walking the binary's ancestors. A release binary (--features embed-seed, the next
section) takes neither development rung — it pairs only with the copies inside it, so the
current directory never decides which compiler it is — and prints one stderr line when
$VL_STD or $VL_COMPILER_WASM overrides that. vl --version names the seed, the std, the
digest of each and the rung each came from; vl std says which std this run will use.
Both halves of that rule are paid for: a vl on PATH once paired a master seed with a
checkout 37 commits behind (D1573), and one binary was two compilers depending on cd
(D1574). See docs/internals/cli-design.md.
vl in another projectCopy one file. A release vl carries its own std and its own compiler seed, so a pinned
toolchain is the binary and nothing else:
mkdir -p .vl && cp "$(command -v vl)" .vl/vl
.vl/vl --version # the commit, the seed bytes, the std digest
vl std --dump .vl/std writes the std sources out if you want to read or diff them — they
are not needed to run anything, and a std/ sitting beside the binary is ignored (vl says
so once). vl std --list and vl std --hash say what is inside. Set $VL_STD only when you
are hacking on std itself; vl will tell you it is in effect, and vl --version prints the
digest of the std in use beside the one the binary was built with.
A release binary bakes the seed in, so it has no build/vl-compiler.wasm to keep a
.cwasm sidecar beside. It caches the seed's compiled form under $VL_CACHE_DIR →
$XDG_CACHE_HOME/vl → $HOME/.cache/vl → %LOCALAPPDATA%\vl instead: the first run on
a machine pays a few seconds to compile the seed, every run after starts in milliseconds.
The cache is content-keyed, self-healing and size-bounded (a few seeds' worth; older
entries are retired as new ones land) — delete it and the next run rebuilds it, and if
the directory is unwritable vl still works, just without the speedup.
The modules your programs compile to are cached the same way. vl run x.wasm,
vl run x.vl, vl run --batch and vl test keep the engine's native compile of each module
under modules/ in that same directory, keyed on the module's bytes (SHA-256) and the engine
configuration, so running an unchanged program again skips the compile — 0.3–0.5 s on a
100 KB module, most of a short run. Each entry carries a digest the host checks before
loading it; one that fails is recompiled and rewritten. The directory is pruned back to
$VL_CACHE_MAX_MB (default 512) least-recently-used first, at most once a minute.
VL_NO_CACHE=1 turns this cache off (the compiler's own cache above is unaffected), and
VL_CACHE_TRACE=1 prints hit / miss / rejected (…) per module on stderr. On Unix, vl
uses a cache directory only if you own it and neither group nor others can write it (it
creates its own with mode 0700); anything else runs uncached, since a file planted there would
be loaded as native code. On Windows the default %LOCALAPPDATA% is already per-user.
$VL_GC picks the garbage collector vl run gives your program — a pure runtime
tuning dial, with no effect on what the program computes:
auto (default; tracing, collects cycles) · tracing · refcount (shorter pauses,
much lower throughput, cannot reclaim cycles) · none (never collects; traps when
the heap fills — short batch runs only). It applies to the vl host; a module you
ship to a browser gets whatever that engine provides.
Background and measurements: docs/internals/memory-gc-design.md.
A self-contained release binary bakes the seed in, so a shipped vl is one
file that runs anywhere with no out-of-band asset:
scripts/fetch-seed.sh # seed into build/
scripts/build-binary.sh # → dist/vl (host); --target <T> or --all to cross-build
(scripts/build-binary.sh wraps cargo build --release --features embed-seed; the
release workflow builds every target this way. deno task compile is an alias for
the host build.)
| Command | What it does |
|---|---|
vl run <file.vl> | Compile and run; program output goes to stdout. Also vl run -e "<src>", … | vl run (stdin), or vl run <prebuilt.wasm>. --extern NAME=VALUE gives an extern let/extern const global its value. |
vl build <file.vl> | Compile to WebAssembly (-o <out.wasm>; -O optimize, -O3 the release profile, --wat also dump text, --no-validate skip the instantiability check; --import-memory / --heap-base= / --heap-limit= lay a unit out to share a host-owned memory; --shared-memory=<pages> makes that memory shared across Web Workers; --initial-memory=<size> / --max-memory=<size> size it). |
vl check <path> | Type-check + lint (errors + warnings/hints) a file, or every *.vl under a directory (recursive; vl check ≡ vl check .; --exclude <glob> prunes paths). Pretty output (carets, TTY color) or --concise. --severity <hint|info|warning|error> gates the exit code + display floor; --codegen also runs the emitter; --fix writes the safe lint fixes in place — prefer-const (let→const) and redundant-type (removes an explicit annotation the initializer already infers, e.g. let x: i32 = 5 → let x = 5). |
vl fmt <path> | Format (AST-driven, via format.vl): stdout, -w write in place, --check CI gate, dirs recurse. |
-O / -O3 (optimize) and --wat (text dump) shell out to binaryen (wasm-opt / wasm-dis) on the
PATH — brew install binaryen, or the prebuilt releases
(or point $VL_WASM_OPT / $VL_WASM_DIS at them). Absent, each is a soft no-op with a note.
-O3 is the release profile, and it is a flag set rather than a level:
wasm-opt --closed-world -O3 --gufa -O3. It is what makes per-tick scratch
allocations disappear — a union box built by a helper and narrowed in a loop keeps
all 4 of its allocation sites under -O and has 0 under -O3, because the
lever is --closed-world and not the optimization level. -O is unchanged and
stays the fast edit-loop rung (on a 1.1 MB module -O3 is 60% slower and slightly
larger). The measurements, the flag-by-flag audit, and the two scratch shapes that
do not melt: docs/internals/opt-profile-design.md.
vl build validates the module it wrote and exits non-zero if the engine will not accept
it — a module that cannot instantiate is a compiler emit bug, and blessing the artifact anyway
sent callers off with a .wasm that only fails later, at vl run. The check reads the file back
off disk (so -O output is covered too), runs after --wat (a broken module is exactly the one
worth disassembling), and leaves the file in place — the exit code, not a missing artifact,
is the signal. --no-validate opts out and restores the old write-and-bless path, for when you
want the bytes regardless. The opt-out is an exact-string match, so a misspelled flag still
validates: the fail-safe direction is to check.
| code | meaning |
|---|---|
0 | success — the program ran, the module was written, the file is clean |
1 | your program failed: a compile diagnostic, a runtime trap, a failing test, fmt --check drift |
2 | usage — an unparseable command line, an unreadable input, an unknown flag |
3 | vl fmt only: an internal formatter bug (the formatted output failed to re-parse) |
70 | the compiler itself crashed. This is a vl bug, not your program's |
A wasm trap renders the same whichever module raised it, so a crash inside the
compiler used to reach you as wasm trap: out of bounds array access over anonymous
frames — indistinguishable from your own array index being out of range, and the first
external VL consumer lost a session to exactly that. Exit 70 and a banner now say so
outright:
vl: the compiler itself trapped while compiling decode.vl. This is a bug in vl, not in your program.
please report it with this file and the output below (vl 0.1.0, seed 1832652 bytes)
Everything after the banner is the original error, unchanged. If you see it, the file you were compiling plus that output is the whole bug report — please open an issue. Nothing you can write in VL is supposed to produce it.
The brains live in VL (the seed); vl is a thin host (a command-queue pump — all
CLI policy is VL, see docs/cli-design.md). A test
subcommand is planned (see docs/test-runner-design.md).
The compiler compiles itself: the seed compiles the current compiler/*.vl into a
new seed, and the result is a byte-for-byte fixpoint.
scripts/refresh-compiler.sh # rebuild the seed from current source (self-compile)
scripts/native-fixpoint.sh # prove stage3 == stage4 byte-identical
The seed is fetched from a published release artifact via
scripts/fetch-seed.sh (republished on every master push; the immutable seed-v0
release anchors the lineage). There is no TypeScript genesis path — the project
keeps no second compiler. Full design in
docs/genesis-design.md.
The test suite, the LSP, and the playground run on deno + npm. Install pinned deps once, then use the tasks:
deno task install # = npm ci
| Task | What it does |
|---|---|
deno task test | Run the suite (tests/ — the .vl corpus + unit tests). |
deno task playground | Build the in-browser playground (Monaco + client-side LSP) and serve it. |
deno task playground:build / :verify | Build / verify just the playground bundle. |
Everything above runs natively on Windows, with Git for Windows supplying the shell.
npm ci).C:\Program Files\Git\bin on PATH ahead of System32. A bare bash otherwise
starts WSL..sh scripts under Git Bash: bash scripts/gate.sh, bash scripts/refresh-compiler.sh.<drive>:\tmp on the checkout's drive. Several fixtures write /tmp/..., which a
native process resolves on the current drive.vl prints paths with / separators on every OS, so compare test output against
slashed() from tests/support/filePath.ts.VL_TMP to it: gate.sh then puts every row's
temp files there. A scanner that inspects each process launch and new file otherwise costs
the gate about 10–15% of its wall time.bash scripts/gate.sh --fast is a local check to run before pushing: every row except the slow
ten. It takes about a minute on Windows and is not the merge gate.gate.sh runs at most 6 rows at a time on Windows unless GATE_MAX_PAR says otherwise.
Measured on a 24-thread box, 2026-10-12: every row at once 501 s; 12 at a time 342 s;
8, 332 s; 6, 306 s; 4, 333 s.VL_BINARYEN to a directory of binaryen's native release (the version in
package.json) and the suites use it instead of npm's JS build. The output is
byte-identical, -O is about 5x faster, and each call skips node's start-up. On Linux
and macOS gate.sh sets it itself from scripts/fetch-binaryen.sh, which downloads the
pinned release once into ~/.cache/vl; Windows has no pinned build there.gate.sh's CPU column reads 0 here. Git Bash's times cannot see a native child's CPU,
and no gate grades on that column.The language server lives in lsp/. It type-checks via the wasm seed in-process,
selectable with the vital.checker setting (ts | wasm | both).
| Path | Contents |
|---|---|
compiler/ | The self-hosted compiler — lexer.vl, parser.vl, typecheck.vl, wasmEmit.vl. |
scripts/vl-host/ | The Rust vl host (wasmtime embedding). |
scripts/ | Bootstrap + fixpoint scripts (fetch-seed.sh, refresh-compiler.sh, native-fixpoint.sh). |
std/ | The VL standard library (std: modules, e.g. std:fmt). |
tests/ | The .vl corpus (tests/cases/) + unit/integration tests. |
lsp/ · playground/ | Editor server and in-browser playground. |
docs/ | Docs — guide/ (language & CLI) and internals/ (compiler & contributor); see docs/README.md. |