Mahdi
All projects

vlip

Lisp asks you to write macros for things other languages treat as syntax. That is a real ergonomic win and a real cost: programs stop being readable without knowing which forms are macros. So the question was whether the readability comes from macros or from what the language already does — and, if it does not have to be macros, which three modern additions are worth having without inheriting a whole language's baggage to get them.

VLispCEK machineTail calls
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.The bet: you should not need macrosMost Lisps ask you to write macros for things other languages treat as syntax. That is a real ergonomic win. It is also a real cost — the program stops being readable unless you already know which forms are macros.So vlip tries a different arrangement. Take the ergonomics that modern Lisps worked for, and provide them as language features instead:FromWhat it removesmatch*RacketNesting one match inside another to compare two valuesuseGleamThe staircase indentation of callback chainsOpaque typesGleamEncapsulation without classes, interfaces or inheritanceBecause these are forms the reader knows, a program reads the same to someone who has never run it. No macroexpansion required.Tail calls are structural, not a flagExecution is a CEK-style machine with an explicit continuation stack. That buys proper tail calls today and fibers later, without redesigning the evaluator.The test suite prints the stack depth at the end of every run:
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)
A quarter-million tail calls, and the stack ends at zero. The frame in tail position gets reused rather than pushed, so a loop stays a loop.tests/non_tail.v exists for the opposite reason. The bug where a call that should have been a tail call pushes a frame instead is easy to write and invisible until the program runs out of memory.The reader is where the language actually liveslet* does not work yet, and the reason is more interesting than a missing feature.In most Lisps [...] is unambiguous: a binding group in form position, a vector in value position. vlip wants [...] to be both, so a binding form and its initialisers read the same way.That only works if the reader knows which reading applies before it has finished reading — and it does not yet. So let* is blocked on the reader, not on the evaluator. docs/010-roadmap.md names what else is missing: rest parameters and callable keywords.Four findings from building the value representationThe Value type is shaped the way it is because of four things found along the way. Three of them are limits of the language rather than design choices, and all four are verified by programs in src/.A boxed sum type cannot hold a pair
struct cons_v { head int
               tail Lst }
// error: invalid recursive struct `cons_v`
A cons cell is recursive by definition, so the representation V would hand over for free would need an extra allocation on top of the boxing.A sum-type field cannot be set from a localNot in a loop, not in recursion — v := cons_v{head: x} errors with x evaluated but not used, while the identical plain struct compiles and runs.That looks like a checker bug, and it is worth reporting upstream. Either way it rules the representation out today.A voidptr field is not a GC rootThis was the original design, and it silently corrupted programs. The probe builds a 2,000,000-cell cons chain, churns the heap, then walks it — and 25 cells survived.V derives GC roots from typed fields, and a voidptr does not say what it points at. The fix was to make the payload a typed reference. The same probe then reported 2,000,000 / 2,000,000 with a correct sum.A method-less marker interface holding a struct is brokenIt compiles cleanly, then dies with invalid memory access in both dev and -prod. Adding one method fixes it.Worth remembering anywhere an interface is used as a tagged union: a construct that builds without complaint and corrupts memory at runtime deserves more than a footnote.Where it standsMilestone M3. The reader parses all six example programs, and the machine evaluates closures, arithmetic, branching, loop, dotimes, letrec, cond and case. The tail-call suite is 17 tests and passes. Embedding and the REPL are next.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.The site in the header is generated by vlip itself, in sixteen languages.

Keep reading