- V 85.7%
- JavaScript 6.2%
- GLSL 3.2%
- Dockerfile 1.6%
- PowerShell 1%
- Other 2.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .github/workflows | ||
| docker | ||
| docs | ||
| editors/vscode | ||
| examples | ||
| lib | ||
| programs | ||
| site | ||
| src | ||
| tests | ||
| tools | ||
| vlib/blip | ||
| .dockerignore | ||
| .gitignore | ||
| AGENTS.md | ||
| blip.v | ||
| CHANGELOG.md | ||
| CONTRIBUTING.md | ||
| install.ps1 | ||
| install.sh | ||
| reader_probe.v | ||
| README.md | ||
| server.v | ||
| v.mod | ||
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 onematchinside 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/blipimage, and nodocker runfrom 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.
-
A boxed sum type cannot represent a pair.
struct cons_v { tail Lst }is rejected asinvalid recursive struct. A cons cell is recursive by definition, so the representation V would give us for free does not work. -
A sum-type variant field cannot be initialised from a local.
x := 7thencons_v{head: x}giveserror: x evaluated but not used— no loop or recursion involved. A plain non-sum struct with the same shape is fine. -
A
voidptrfield is not a GC root. The original design stored payloads behindo voidptr, and it silently corrupted programs: a 2,000,000-cell cons chain walked 25 cells after heap churn.Value.payloadis therefore a typedPayloadinterface, which the GC does track. Same probe after the fix: 2,000,000 / 2,000,000, sum exactly correct. -
A method-less marker interface holding a struct is broken. It compiles, then dies with
invalid memory accessin dev and-prodalike.Payloadcarries 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-allocsand-preallocarenas 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.Durationinto nanoseconds. Treat its output as provisional.