A Lisp dialect implemented in V: real tail calls, an embeddable CEK machine, match*/use, and errors as values.
  • V 85.7%
  • JavaScript 6.2%
  • GLSL 3.2%
  • Dockerfile 1.6%
  • PowerShell 1%
  • Other 2.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
metif12 6d8d7e6789
Some checks are pending
ci / test (push) Waiting to run
ci / docker-images (push) Waiting to run
pages / build (push) Waiting to run
pages / deploy (push) Blocked by required conditions
docs: README reflects the repo as it is (status, suites, milestones, install)
2026-10-31 11:07:13 +03:30
.github/workflows chore: audit repo, remove 11 dead files, build.lip canonical everywhere 2026-10-31 10:23:29 +03:30
docker sync V to 74be97a (18 commits ahead of b9ebf8b); clean warnings 2026-10-30 12:19:24 +03:30
docs website v2 and docs pass: md-rendered pages, require path fix, help text 2026-10-31 11:02:16 +03:30
editors/vscode A CLI shaped like blip, package management, and module imports 2026-10-10 16:36:11 +03:30
examples emit: wire defmacro/macex into the backend; 05_pipelines now runs 2026-10-29 09:20:45 +03:30
lib std+machine: array-append!, (defined? name); the emitter drives multi-form clause bodies 2026-10-28 23:46:49 +01:00
programs website v2 and docs pass: md-rendered pages, require path fix, help text 2026-10-31 11:02:16 +03:30
site website v2 and docs pass: md-rendered pages, require path fix, help text 2026-10-31 11:02:16 +03:30
src Rebrand vlip to blip, add the logo and the language server 2026-10-10 14:20:33 +03:30
tests website v2 and docs pass: md-rendered pages, require path fix, help text 2026-10-31 11:02:16 +03:30
tools chore: audit repo, remove 11 dead files, build.lip canonical everywhere 2026-10-31 10:23:29 +03:30
vlib/blip website v2 and docs pass: md-rendered pages, require path fix, help text 2026-10-31 11:02:16 +03:30
.dockerignore chore: audit repo, remove 11 dead files, build.lip canonical everywhere 2026-10-31 10:23:29 +03:30
.gitignore chore: audit repo, remove 11 dead files, build.lip canonical everywhere 2026-10-31 10:23:29 +03:30
AGENTS.md chore: audit repo, remove 11 dead files, build.lip canonical everywhere 2026-10-31 10:23:29 +03:30
blip.v website v2 and docs pass: md-rendered pages, require path fix, help text 2026-10-31 11:02:16 +03:30
CHANGELOG.md website v2 and docs pass: md-rendered pages, require path fix, help text 2026-10-31 11:02:16 +03:30
CONTRIBUTING.md chore: audit repo, remove 11 dead files, build.lip canonical everywhere 2026-10-31 10:23:29 +03:30
install.ps1 sync V to 74be97a (18 commits ahead of b9ebf8b); clean warnings 2026-10-30 12:19:24 +03:30
install.sh sync V to 74be97a (18 commits ahead of b9ebf8b); clean warnings 2026-10-30 12:19:24 +03:30
reader_probe.v Rebrand vlip to blip, add the logo and the language server 2026-10-10 14:20:33 +03:30
README.md docs: README reflects the repo as it is (status, suites, milestones, install) 2026-10-31 11:07:13 +03:30
server.v Rebrand vlip to blip, add the logo and the language server 2026-10-10 14:20:33 +03:30
v.mod Rebrand vlip to blip, add the logo and the language server 2026-10-10 14:20:33 +03:30

blip

blip

A Lisp dialect implemented in V, designed so that ordinary programs are written without macros — while macros remain available for the cases that genuinely need them.

S-expressions and a Racket-shaped surface, plus three things modern Lisps have each solved separately and no single language has combined:

  • match* — atomic multi-subject pattern matching (from Racket). Match two values at once without nesting one match inside another.
  • use — callback capture that keeps chains flat instead of staircasing the indentation (from Gleam).
  • Opaque types — encapsulation by export metadata, with no class system, no interfaces, and no multiple inheritance (from Gleam).

Execution is a CEK-style machine with an explicit heap stack, which gives proper tail calls today. Fibers are designed for — the heap stack makes them possible without a representation change — but are not yet implemented; yield exists as an identity hook.

There is no static type system in v1. Type information exists as optional, erased annotations plus runtime contracts, and both are opt-in per program.

Status

The machine runs real programs with real tail calls: the reader parses all six example programs, and the machine evaluates closures, arithmetic, branching, loop, dotimes, letrec, cond, case, match/match*, macros, structs and modules.

Working on top of that: a REPL with golden tests, errors as values with source-context frames, real programs in programs/ (brainfuck, json, config, csv, lint, markdown, pratt), a JavaScript backend (blip build, blip emit-js), a veb bridge that serves the website from blip handlers, machine-generated prims (tools/genprims.vsh), and editor support (blip lsp plus a VS Code grammar/client). Recent changes are recorded in CHANGELOG.md.

docs/000-blip-design.md the design document: motivation, language, architecture, trade-offs, comparisons
docs/010-roadmap.md embedding, the REPL, the example programs to write, and the veb final phase
vlib/blip/ the implementation: value representation, reader, printer, machine, primitives
examples/ the six programs that define the syntax; all parse
tests/tail_calls.v tail calls, including mutual recursion, stay flat
tests/non_tail.v non-tail sibling calls, the case the environment bug hid in
tests/loop_forms.v loop, dotimes and letrec
tests/binding_forms.v let, let*, letrec, rest parameters, shadowing, no leaking
tests/embedding.v the machine as a value: independent machines, limits, surviving bad programs
tests/repl_golden.v the REPL in-process plus a stdin-to-stdout golden fixture
tests/examples_run.v all six examples run to their markers
tests/programs_run.v all seven programs run to their markers
tests/emit_js.v the JS backend: examples emit and run identically under node
tests/lsp.v diagnostics over stdio agree with blip run
tests/hostile.v hostile input becomes values, never panics
tests/api_coverage.v every public function is engaged by a test or program
tests/fibers.v yield round-trips a value through the machine
tests/veb_bridge.v a blip handler closure registers and dispatches
tests/genprims.v every generated prim's value, arity, and type errors
tests/multi_use.v use loads into the one running machine: definitions cross the boundary
tests/value_to_datum.v value_to_datum: values convert back to AST nodes (the macro path's inverse direction)
tests/macros_gate.v the embedded-machine macro path: pure shapes convert end-to-end, complex shapes refuse by error

let*, rest parameters and callable keywords now work, and tests/binding_forms.v covers them. The three were long misdiagnosed as one problem — "[...] is a vector literal in a value position and a binding group in a form position" — and were actually two unrelated parser bugs: let* started its recursion at binding 1 and so silently dropped the first binding, and . was parsed as an ordinary parameter named .. [...] is resolved by position, not by a reader flag: in the second slot of let/let*/letrec it is a binding group, and everywhere else it is a vector literal. Both [a 1] and (a 1) are accepted as a binding, because the examples and the documentation disagree about which to write and the examples win.

Verified

ok   self tail call 250k => 250000   (steps=7000020, kont=0)
ok   mutual tail call 2e4 => pong   (steps=400020,  kont=0)
ok   tail in cond 1e5 => 100000     (steps=2900021,  kont=0)
ok   non-tail fib => 6765           (steps=503488,   kont=0)
ok   loop => 7                      (steps=185,      kont=0)

A stack depth of 0 at the end of a quarter-million tail calls is the whole point: the frame in tail position is reused rather than pushed.

Install

Requires the pinned V toolchain — vlang/v master at the commit in .github/workflows/ci.yml (the install.sh/install.ps1 scripts carry the same pins, and tests/unit.v fails if they drift). Use -cc gcc: the default C backend fails on this checkout, and CI uses the same flag.

macOS & Linux

curl -fsSL https://metif12.github.io/blip/install.sh | bash

Windows

irm https://metif12.github.io/blip/install.ps1 | iex

Docker

Three toolchains, each building the pinned V compiler from source, in docker/. CI builds and runs all three on every push.

docker build -f docker/alpine.Dockerfile -t blip:alpine .
docker run --rm blip:alpine run examples/01_basics.lip

docker/debian.Dockerfile and docker/nix.Dockerfile build the same image for the other two toolchains. Use -f with the file you want and -t to name it; the three are interchangeable and differ only in the C toolchain.

The images are built and exercised by CI but are not published to a registry. There is no ghcr.io/metif12/blip image, and no docker run from a remote will work until one is.

From source

git clone https://github.com/metif12/blip.git && cd blip
v -cc gcc -o blip blip.v

The v there must be the pinned commit (see above) — a newer or older compiler is how a working checkout breaks. install.sh does the whole thing: V plus vc at their pins, then blip.

Build and test

./blip run examples/01_basics.lip

v -cc gcc run tests/tail_calls.v       # a suite, direct from source
v -cc gcc -o reader_probe reader_probe.v   # every example parses
./reader_probe examples/*.lip

Four findings from M0

All verified on V 0.5.2 by the programs in src/. These are the reason the Value representation looks the way it does.

  1. A boxed sum type cannot represent a pair. struct cons_v { tail Lst } is rejected as invalid recursive struct. A cons cell is recursive by definition, so the representation V would give us for free does not work.

  2. A sum-type variant field cannot be initialised from a local. x := 7 then cons_v{head: x} gives error: x evaluated but not used — no loop or recursion involved. A plain non-sum struct with the same shape is fine.

  3. A voidptr field is not a GC root. The original design stored payloads behind o voidptr, and it silently corrupted programs: a 2,000,000-cell cons chain walked 25 cells after heap churn. Value.payload is therefore a typed Payload interface, which the GC does track. Same probe after the fix: 2,000,000 / 2,000,000, sum exactly correct.

  4. A method-less marker interface holding a struct is broken. It compiles, then dies with invalid memory access in dev and -prod alike. Payload carries one method for this reason.

Findings 1 and 2 are worth reporting upstream to the V project.

Roadmap

# Deliverable
M0 skeleton, Value representation, benchmarks — done
M1 reader: lexer and S-expression parser, spans, collected diagnostics — done
M2 values, printer, equal?, hash — mostly done
M3 CEK machine — gate: 1M-call fib, 100k-deep reverse, no silent stack overflow — tail-call gate passed
M4 special forms, core forms, 100+ primitives — done
M5 macros: defmacro, gensym, quasiquote, macex1/macex — done
M6 modules: require/provide, only-in, prefix-in — done (for-syntax deferred)
M7 standard library written in blip — done, growing (lib/std.lip)
M8 ergonomics: match, match*, use, pipes, labelled args, Result, opaque types — done (gate program: 311-line programs/json.lip, macro-free)
M9 tooling: REPL, span-carrying errors, formatter, assert as doc-tests — done
M10 contracts and optional erased annotations — open

The embedding plan, the REPL design, the example programs worth writing, and the veb-backed final phase are in docs/010-roadmap.md. The ordering there differs from this table in one respect: errors become values before macros do. A panic inside an embedded interpreter unwinds through the host and kills it, so that has to be true before anything can call blip from V, and certainly before Lua.

Section 7 of that document is the upkeep checklist: syncing against a new V, re-running the representation probes, and keeping the known-broken list honest. The compiler sync is the one that matters most, because this project does not target the 0.5.2 release — it targets vlang/v master at the commit pinned in .github/workflows/ci.yml, which resolves vlib.blip.* against the project's own modules where the release build resolves it against V's standard library.

Branches and releases

Branch Role
main stable. Releases are cut from here by pushing a v* tag. Protected.
dev integration. Pull requests land here.
<topic> a branch off dev

main moves only when a milestone's gate is met, so it stays something a person can depend on. release.yml verifies on the tag — the same seventeen suites a contributor runs locally — and builds the binaries and the generated site. A manual run of that workflow builds and verifies but does not publish, so a dry run cannot create a release by accident.

Contributing

See CONTRIBUTING.md. The short version: branch off dev, use -cc gcc, keep all seventeen suites at zero FAIL lines, and never hand-edit the website — it is generated by site/build.lip from site/content/*.md through site/layout.html.

The website

Generated, English-only for now, at https://metif12.github.io/blip/.

Design notes worth knowing

  • The value representation was decided by the host language, not by taste. The design document originally promised a three-way benchmark. Measurement turned out to be beside the point: two of the three candidates cannot be written at all on V 0.5.2.
  • v -warn-about-allocs and -prealloc arenas are used from the start; V documents the latter as intended for compilers.
  • No generics. V's own compiler skips monomorphization when building itself (vlib/v/pref/pref.v:57) for build speed; blip does the same.
  • The benchmark harness is not yet trustworthy. It produced three wrong numbers before being fixed, including a 1000x unit error from coercing a time.Duration into nanoseconds. Treat its output as provisional.