-
released this
2026-10-24 19:18:18 +02:00 | 0 commits to main since this release- lamp builds as a fetched dependency again: 0.3.0's package (what
zig fetchtakes, build.zig.zon's.paths) had nobuild_native.zig
and none of the test outputs the build reads while configuring, so a
project depending on it by url and hash failed before building
anything. The gate now builds lamp as a fetched package.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- lamp builds as a fetched dependency again: 0.3.0's package (what
-
released this
2026-10-24 17:49:11 +02:00 | 4 commits to main since this release- Replacing a native unit while the session runs:
Session.reloadNative(path)loads the rebuilt .dylib atpathover the
unit loaded from it; the new definitions take over the names, and
procedures kept from the old one run its code. The old library is closed
by the first full collection after nothing needs its code
(Session.replacedNativeUnitscounts them).loadNativeof a path
already loaded is nowerror.NativeUnitLoaded. Session.can_suspend: whether.stops = .suspend_can suspend a
call here -- macOS on arm64. Elsewhere it is.hook, as before, and no
longer tries to make a fiber it cannot guard.- A .dylib loads into an engine built in any optimize mode: compiled
code read the runtime's heap at offsets Zig lays out differently in Debug
and in release builds, so a unit from a Debuglamp-nativewas refused
by a ReleaseFast engine ("compiled against a different runtime library").
What it reads is now in extern blocks with one layout. - Faster string loops in compiled code:
string-refon a string with
a character past Latin-1 (a wide string) andstring-lengthrun inline;
both went through the interpreter's handler. So dochar-alphabetic?,
char-numeric?,char-whitespace?,char-upper-case?and
char-lower-case?for an ASCII character. A word count over a text with
non-ASCII in it ran 6x faster. - A native used as a value is one procedure in compiled units too: a
unit compiled bylamp-nativemade its own closure for a native it
passed as a value, soeq?against the session's was#f(and a host
keying anything on the procedure missed it). A unit now takes the
session's, or leaves its own there for the session. lamp-nativefor a dependent's build:lamp_dep.namedLazyPath("lamp-native")
is the compiler, for a build step that compiles its Scheme to a .dylib.lamp-nativereplaces its output instead of rewriting it: the new
file is written beside the old one and renamed over it. A running
session that loaded the old .dylib keeps its code intact, and loading
the path again opens the new one.- Native units as a .dylib:
lamp-native compile --unit --dylib unit.scm unit.dylibwrites a signed dynamic library -- by itself, no
linker or Zig needed -- andSession.loadNative("unit.dylib")loads and
runs it on the session, asrunNativeruns a linked one. - A native unit's entry takes the runtime's table: a unit object has
no undefined symbols now -- its calls of the runtime go through a table
Session.runNativepasses it -- the first step to loading one as a
.dylib. A host declares the entry with a second parameter:
extern fn lamp_unit_main(host: *anyopaque, table: *const anyopaque) callconv(.c) c_int(Session.NativeEntry). Recompile units. set!of a function defined earlier in a session: at the REPL (or
byload/eval),(set! f ...)after(define (f) ...)failed with
"UnknownVariable". It now assignsf, and functions compiled before
call the new value -- a procedure, or an error if it is none. Wrapping
the old one,(set! f (let ((old f)) ...)), works; a prelude function
is shadowed for the REPL, asdefinedoes.- A native unit's own function used as a value is the session's:
f
passed or returned by the unit's code was a private closure -- not
eq?to the session'sf, and still the old function after the
session redefinedf. It is now read fromf's binding. And two
compile-time assumptions about the unit's top-level functions no longer
hold once the session can redefine them: that a function handed a
continuation never keeps it (re-entering it failed with "invoked after
its call/cc returned"), and that one captures no continuation (a list
built by recursion could be changed after it was returned). - A function's own tail call sees a redefinition made during it: in a
session (the REPL,load,eval, an embedding), a function redefined by
something it called --(define (f n) (g n) (f (- n 1)))withg
redefiningf-- went on looping in the old definition; it now calls the
new one, as R7RS reads the name at every call. Native units likewise, for
tail calls and other calls of itself. Only where a real call can come
first: a loop over primitives, orfib, runs exactly as before. - A native unit sees a redefinition of its own functions: a function
of the unit's top level redefined by the session (define,set!,
load) is the one the unit's other functions call from then on; they
called the compiled one. A library's functions in a unit are not
affected, as R7RS lets no importer redefine them. A small top-level
function is still inlined into its callers in the unit, behind a check
of its variable: a loop calling(define (inc x) (+ x 1))runs as fast
as in a library (81 ms against 86), where without inlining it took 247. - A native unit's function given a continuation keeps its parameter:
when every call in the unit passed it acall/cc's continuation, the
compiler dropped the parameter, and the session's calls of the function
failed with "wrong number of arguments". - Several native units in one session:
session.runNativeruns
each unit a host links (lamp-native compile --unit --entry NAME
names each one's entry). The units call each other's functions
directly -- 3.4 ns a call, against 2.6 ns inside one unit -- by name
or as values, and theirguards, handlers and continuations work
across each other's code. A unit reads another's function from its
variable at each call, so a new definition reaches it. - A native unit with nothing at its top level but definitions can be
called: the session's calls of its functions failed with "a native
unit's procedure before the unit ran". - A native unit carries only the prelude it uses: a unit compiled
withlamp-native compile --unitheld the whole prelude, 109 KB of code
for a one-line unit; it now holds what its code can reach (980 bytes
for that one). - Continuations cross a native unit's boundary: the unit leaving a
for-eachof the session's early, a callback the unit calls leaving
through it, and the reverse, go back to where the continuation was
captured, unwinding both sides' frames,guards anddynamic-winds
between; one may be re-entered from the other side while the call
that captured it runs. One resumed after that call returned is an
error that says so. - A continuation called in a host native's callback goes back to where
it was captured; it could crash the VM, or resume inside the callback
and return to the native.call/ccinside such a callback is refused also when
the native was called from the top level, as it was everywhere else. - A native unit and the session call each other to any depth: the
session's code may call back into the unit while the unit is calling
it, and a host native called from the unit may call closures of
either (session.call). Araisereaches the innermost handler or
guardon either side, with the object raised: the handler stack and
thedynamic-winds are now one for both. Anapplyof a list longer
than the VM's window to one of the unit's variadic procedures passed
one argument and upset the VM's next variadic call; it is spread as
it should be. - A procedure of no parameters rejects arguments in the VM:
((lambda () 1) 5)returned 1 when the lambda was called through a
variable or passed as a value, and is now a "wrong number of arguments"
error, as it already was for every other arity. - A native unit calls the session's procedures: a lambda passed to it,
a procedure such aslistas a value, and a function the session
defined, by name. The session's VM runs them nested; an error there
reaches aguardin the unit, and araisereaches aguardof the
session's below the unit. Calls between the unit's own functions are
unchanged: the session's take the path that used to refuse them. - Steps in callbacks a host native makes. When Scheme calls a
native that calls back into the session once per entity (a frame
written in Scheme), a step that leaves a callback (step_out, or
running off its last line) now stops at the native's next callback,
then at the caller's next line, compiled or not. Before this,step_out
ran to the end of the frame, and a compiled caller never stopped.
Stepping over the native's line no longer stops inside its callbacks.
The abort fault now reads "aborted while stopped in the debugger". - Breakpoints and steps in library code. Code from a library loaded
from the search path, from an inlinedefine-library, or from an
included file now keeps its own file and lines. A breakpoint in
my/util.sldbinds when the library loads, a step goes into it, and
backtraces and the editor showutil.sld:6. A library file is named
by its real path. Before this, such breakpoints never bound and steps
ran through. Session.runSuspendable(f, ctx): in suspend mode, a stop in any
callback the host's script phase makes --on-messagein its message
passes,on-collision-- suspends the phase itself, the host's own loop
with it.resumeStopgoes on from the stopped instruction and returns
when the phase does. Nothing is skipped, repeated or reordered; a new
phase while stopped iserror.SessionSuspended.resumeStop's error
set is nowanyerror(a phase's own error comes back through it).
docs/EMBEDDING.md, "Stops in every callback, and order".dap.Adapter: the debug adapter without a transport, for a host
with its own threads and framing -- one request in, its responses and
events to a sink.dap.Serveris now the socket and stdio framing over
it, used as before (server.launch_whyisserver.adapter.launch_why).
In suspend mode the editor'scontinueor step is the host's to carry
out (takeResume), from its frame loop;hostStopped/hostContinued
tell the editor of the host's own pauses, and logpoint messages are
outputevents. Conditions, hit conditions and logpoints come through
setBreakpoints; a breakpoint that binds or moves on a load is told
as abreakpointevent. Compound values open in the variables view
(lists, vectors, records, pairs; long ones capped), a host adds scopes
of its own (adapter.scopes_fn), andevaluatetakescontext
(hoverreads a name without compiling) and runs at top level when no
frame is named while suspended.adapter.output(category, text)sends
a host's own output to the editor's debug console. The session's debug events addstoppedand
continued(suspend mode).- Breakpoint conditions, hit counts and logpoints, in the session
(s.inner.setBreakpointWith(file, line, .{ .condition, .hit_condition, .log_message })), so a host without DAP has them too. A condition is
compiled once per breakpoint, not at every hit. A logpoint's message goes
tos.inner.onDebugEvent; it never stops. - Breakpoints follow their source file: a hot reload keeps them, and
one set before its file is loaded binds when it is (setBreakpoint
returnsnullfor it, and abreakpointdebug event follows). Before,
a reload's new code had none, and an unloaded file's were refused. - Debugger stops that return to the host (experimental, arm64):
Options.stops = .suspend_makes a call that stops return
error.Suspended; the host inspects the stop (Session.stopped) and
goes on later from its own frame loop withSession.resumeStop--
continue, step or abort. Meanwhile it may still evaluate, in a stopped
frame or at top level, and reload code.Session.debugScopesays which
calls are debugged (a game's focused entity); a step that leaves its
call stops in the next call in scope. See docs/EMBEDDING.md, "Stops that
return to the frame loop". - Fixed:
pause()did not stop in a compiled procedure the host then
called -- the step only ever saw interpreted code; the procedure is now
taken out of compiled code for the step. And a step no longer outlives
the host call it was taken in. - Fixed: a
guardin a procedure the host called withSession.call
(orcallWith) crashed -- a bus error once it caught something: the
call used the catch storage of the last run, in that run's finished
frame. A host call now starts as a run does, with the session's own
catch stack and an empty frame stack (a call after a stack overflow
also faulted again). (- v)on a native type:native.Type's.negoption, functions
of one argument (fn (Vec2) Vec2). Without it(- v)needed a.sub
overload taking a number on the left.- Native-type values from the host:
callWithand the new
Session.boxmake a struct the session registered as a native type into
that type (it was silently a list of its fields), anddecodereads one
back. A native's struct parameter or result may be another library's
type. A native may return a tagged union (its active payload), and read
a rest argument with a parameter's rules and refusals (c.as(T, i)). - Fixed:
callWithcould lose aValueargument to a collection while
it built a later one; anisa.Valueargument also became a list. - A host can write top-level variables:
Session.setGlobal(name, value), as(define name value)at the REPL, superseding a function of
that name;Session.slot(name)is a handle to read and write one
without a lookup.global(name)of a function no longer compiles a unit
on every call. - Fixed: after
(define f #f)and a new(define (f) ...),fas a
value was still the old function (andglobal("f")returned it): the
old function's one closure was reused. Now the new function is. - Natively compiled code can run inside an embedder's session
(experimental, static linking):lamp-native compile --unitand
Session.runNative; the unit's globals are the session's by name, and it
can call the host's natives (Session.writeNativeInterface,
--natives). The session can call the unit's procedures, its
functions by name and the closures it returns; the unit cannot call the
session's yet. See docs/EMBEDDING.md, "Compiled ahead of time". - A library's own definitions stay its own: a program that leaves an
export out of its import ((except (my lib) f)) and defines its ownf
no longer replaces the one the library's code calls (R7RS 5.6.1). A
library's body also sees another user library through its own import
sets (prefix,rename,only,except). - A program may no longer redefine or
set!a name it imported
(R7RS 5.2):(import (scheme base)) (define (square x) ...)is refused,
naming the library, in both engines; leave the name out with(except (scheme base) square)or rename yours. The REPL and
Session.evalTextstill permit it. Without an explicit
(import (scheme base))a program may define base's names as before. - Native backend:
displayandwriteinsidewith-output-to-string
are captured in every function: those the optimiser could not take
printed straight to the console, and theirwritedid not label a
cyclic list. - Reading a file uses far less memory:
cat,wcandtailpeaked
at 290-320 MB and now take 12-39 MB (Chez: 49-97). A file is read into
one buffer of its size, and a large string that outlives a minor
collection no longer makes the heap grow instead of collecting it.
docs/BENCHMARKS.md has a new Memory section (Lamp against Chez). - Memory use and the collector can be inspected:
Session.memoryStats()
(live and size per area, collections, pause times and their histogram)
andSession.onCollectfor an embedder's profiler,Session.requestFull;
(gc-stats)in a new(lamp gc)library;lamp --statsand
LAMP_STATS=1print a summary at exit, for natively compiled programs
too. See docs/EMBEDDING.md. - Native backend: functions the optimiser cannot take (with
call/cc,
guardorapply) allocate their closures inline too (fibc 1.1x
faster). - Native backend: a local procedure does float arithmetic unboxed when
the function that defines it works with floats, even with no float
constant of its own (simplex 1.1x faster). - The collector stops minor collections sooner while a program's data is
growing: paraffins 1.6x faster with a third less peak memory, earley
1.15x (native), nboyer 1.06x.LAMP_GC_SUSPEND=<n>sets the threshold. - VM: a suspended call's dead registers are no longer garbage-collection
roots, so memory a finished call left behind is freed (paraffins peaked at
over 1.6 GB and could run out; it now keeps what it uses). - Fixed: a procedure called with its result going to a parameter slot could
look unused before the call (VM liveness analysis, which the native
backend's garbage-collection maps also use). - Local procedures are compiled as ordinary functions wherever they are
defined -- in aletbody, a lambda, aletrec, a namedletused
from a lambda, or bound bylet/let*-- instead of as closures (in a box
when recursive), the locals they read passed as arguments: direct calls,
which both engines can inline. Native: quicksort 1.7x faster, mperm 1.65x,
gcbench 1.4x (also on the VM), lattice 1.35x, earley 1.12x. - Native backend:
vector-refin a loop resolves its vector once
(pnpoly 1.35x, fft 1.1x faster), and loops are aligned, which removes a
22% swing inackfrom code placement alone. - A built-in procedure used as a value is called directly when the
argument count fits, instead of searching every native's name on each
call: theschemebenchmark 1.4x faster native, 1.3x on the VM. - Native backend: calling a variadic function costs what Chez's does
(no rest arguments or one: 4x and 3x cheaper), andvaluesused as a
procedure returns one value without building anything
((vector-map values v)3.2x faster). - Fixed (native backend): values held across
applyor
call-with-valuescould be freed by a collection inside the called
procedure. - Native backend: a list walk tests each step once --
(null? l)
followed by(car l)/(cdr l)tests for a pair first instead of for
()and then for a pair (takl and ntakl 1.08-1.1x faster). - Lists built by recursion in a
letreclambda or a namedletare built
by a loop too (primes 1.6x faster native, 2.2x on the VM; paraffins
1.25x). mapandfor-eachover a lambda or a known function run as a loop
(no closure, no indirect call per element), andmapbuilds its result
forward where the function cannot capture a continuation: deriv about 1.4x
faster on the VM, 1.25x native.- Removed: the Zig AOT compiler (
addSchemeExecutable, thelamp-aot
module,zig build aot-compile,--emit-zigand the--aot-*,
--native-types,--dump-analysisand--audit-rootsflags). Compile
ahead of time with the native backend (lamp-native); the old flags now
say so. - Native backend: closures allocated inline (cpstak 2.6x, lattice and
graphs about 1.3x faster). - A list built by recursion is built by a loop (
(cons x (f ...))
in tail position, when nothing in the body can capture a continuation):
divrec 1.5x faster native, 2.9x on the VM, and the VM no longer runs out
of registers building a long list this way. - Native backend: no write barrier for storing a fixnum or immediate
(array1 1.6x faster). - Native backend: fibfp 1.45x faster (constants in guard states take no
slot, raw floats kept in registers up to a call). Fixed a fallback that
could resume a float argument from an unset register. call/ccused only as a return is compiled away (ctak about 55x
faster, VM and native); the native backend compiles a continuation that
cannot outlive itscall/ccas an escape (fibc 1.9x).- Native backend: deep self-recursion no longer mispredicts every
return (a function's one self call is kept off the CPU's return stack):
divrec and primes about 1.55x faster. - Fix: a call or
vectorwith many arguments in a busy function. When
its arguments ran past register 63 they silently overwrote live values (a
wrong answer); such a call is now compiled to fit, and such avectoris
built element by element. - Native backend: Ackermann-shaped functions run as one loop (a self
call as an argument of a self tail call, integers only) over a stack of
their own (ack 5.5x faster). - Native backend and VM: plain ASCII file output is written straight
out, and nativeread-char/write-charare inline (cat about 4x, wc
2.4x faster). - Native backend: type predicates,
eqv?, vector/record allocation and
string/bytevector stores inline (gcbench, peval, dynamic about 2x
faster). - Native backend: character I/O about 2x faster (cat, wc).
- Native backend: integer loops run without type tests (sum 2x faster).
guardre-raises as R7RS says. With no clause matching, the object
is re-raised byraise-continuablefrom the raise itself, so the winds
around it are re-entered, and an outer handler's value returns to a
raise-continuable. Clause tests run outside those winds. (An error a
primitive signals, like(car 5), is re-raised from the guard; the AOT
re-raises every one from the guard.)- Native backend: float loops that start from an argument or a captured
variable run unboxed (sumfp 6x faster), and closures speculate too. - Fix:
applyandcall-with-valuescall in tail position. As R7RS
requires: a loop through either used to fill the stack after about a
million iterations. - Fix:
exact-integer-sqrtin a full heap. A program doing heavy
bignum arithmetic could stop with "%isqrt: no room" (thepibenchmark at
full size); the root now collects and grows the heap as*does. - Fix:
guardrunsdynamic-wind's exits. A raise or an error inside
adynamic-windthat aguardcaught skipped the wind'safterthunk; it
now runs before the guard's clauses, as R7RS says. - Fix:
(flush-output-port)with no port. The VM refused it at
compile time; it now flushes the current output port, as R7RS says. - Embedding:
decodetakes typed shapes. Slices from lists, structs
and tuples from lists of their fields (a 2-tuple from a dotted pair too),
native.Pair, optionals and tagged unions -- the first variant that fits
-- with refusals that name the element: "element 2: expected a symbol or
an integer from 0 to 15, got 99". - Embedding fix:
evalTextandevalSourcefrom inside a native. A
unit evaluated by a native of the same session -- a REPL key -- ran over
its caller's registers, so a loop around the native saw the unit's
result in its counter. It now nests ascalldoes; so doesglobal
when it compiles a lookup. - Natives: a library entry can carry a signature and doc.
.{ "move-left", f, "(move-left [count])", "Move the cursor left." };
docOf,symbol-docandsymbol-signatureanswer them. - Embedding: names for completion.
Session.definedNameslists what a
session's units defined at top level, andlamp.embed.keywordsthe
special forms; libraries and docs come from(lamp meta)through
decode. - Embedding: a session's output can go to a host writer.
Options.outputandSession.setOutput(w)senddisplay,writeand
the rest to a*std.Io.Writer-- a REPL buffer, a game console, or
anything but stdout in a Zig test binary, whose stdout is the build
runner's protocol. - Embedding:
Session.callWithbuilds a call's arguments from Zig.
s.callWith(f, .{ "query", 3 })-- strings, numbers, symbols, lists,
pairs -- wherecallneeded ready-made values and a host had no way to
make a string. - Natives: a library may have hundreds of natives. Adding a large one
stopped compiling with "evaluation exceeded 1000 backwards
branches", a limit only Lamp could raise; it now scales with the library. - Embedding: a native may call back into its own session.
Session.callfrom inside a host native runs nested, asevaldoes,
instead of overwriting its caller's registers; a fault inside reaches the
caller (and itsguard) or is cleared by the native witherrorMessage. - Natives:
native.Pair(A, B)results are dotted pairs.(cursor)can
answer(line . col)and a list of pairs is an alist; a plain struct is
still a list of its fields. - Natives: an optional parameter takes
#fas null. A?Tparameter
is null when its argument is#f, as when it is missing -- a key binding
can always pass its count or#f. For?bool,#fis stillfalse.
Before, an explicit#fwas refused. - Embedding:
Session.decodereads Scheme data into Zig.s.decode(T, v, arena)copies a value -- a native's argument, a callback's result --
intoDatum(any value: lists, dotted lists, strings, symbols, numbers,
and procedures or native values tagged, not copied) or a typed leaf
([]const u8,native.Symbol, integers, floats,bool,Held), with
Lamp's error text when it does not fit. - Embedding: a host can keep any value alive.
Session.hold(v)returns
a handle whose value every collection keeps,held(h)reads it and
release(h)lets it go -- for a lambda a script handed to a native, say.
Before, the only way to keep a value was to put it in a global. - Embedding: a native used as a value is one procedure. Each mention
of a native in value position used to make a fresh closure, so(eq? char-upcase char-upcase)was#f; now a session makes one per
native.Session.globalalso finds a native the session imported from a
library, so a host can keep its natives' values and recognise them by
identity. - VM: float loops run unboxed. A loop that does float arithmetic keeps
its floats in machine registers for as long as it runs, instead of
allocating a heap object for every result:sumfp3.6 -> 0.78 s,
mbrot4.7 -> 1.4 s.LAMP_JIT_NO_FLOAT_LOOPS=1turns it off. - VM: recursive float functions pass floats unboxed. A function whose
arguments and result are floats and that calls itself (fibfp) gets a
second compiled form taking and returning them in machine registers:
fibfp2.3 -> 0.59 s,sumfp0.78 -> 0.47 s.LAMP_JIT_NO_FLOAT_ENTRIES=1turns it off. - VM: small functions are inlined at their call sites. A call of a
short function (an accessor, asquare) runs its body in place behind a
one-compare guard, so a redefinition, a breakpoint or a step into it still
takes effect at the next call; backtraces and error messages still name
the inlined function. Over all benchmarks 2.7% faster (geomean),ray
20%,fibc,conform,simplex,destruc,nucleic,mperm13-15%.
LAMP_NO_INLINE=1turns it off. - VM: recursive fixnum functions call themselves natively. A function
whose arguments and result are fixnums and that calls itself or loops
(fib,tak,ack) gets a second compiled form that keeps its values in
machine registers and calls itself directly; overflow falls back to the
ordinary code, which promotes to a bignum.sum1.9 -> 0.96 s,fib
3.2 -> 1.9 s,tak1.5 -> 0.97 s,ack6.4 -> 4.9 s.
LAMP_JIT_NO_INT_ENTRIES=1turns it off. - VM fix: compiled
+or-that overflowed into one of its own
operands' registers returned a wrong fixnum instead of a bignum -- e.g.
(- acc 1152921504606846975)updatingaccin a loop. The interpreter
was right. - AOT:
readno longer records source lines for the data it reads --
bookkeeping meant for the compiler's error messages, a hash insert per
pair.read10.64 -> 0.40 s. writeno longer clears 20 KB of scratch per call. Its cycle scan
set up its tables for every value, a number included; now only pairs
and vectors are scanned. Theraybenchmark, which writes a number per
pixel: VM 2.45 -> 2.34 s.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- Replacing a native unit while the session runs:
-
released this
2026-10-08 20:51:57 +02:00 | 283 commits to main since this release- AOT: values travel in registers. A value is passed to and returned
from the runtime's out-of-line paths in registers instead of through
memory; a tight arithmetic loop no longer keeps its variables in memory
for a slow path it never takes.sum4.3 -> 1.3 s (now faster than the
interpreter); over all 55 benchmarks 4% faster (geomean). - AOT:
equal?is the interpreter's (shared code now): iterative, with
an interleaved memo, so cycles and shared structures stay cheap; the
equalbenchmark 0.52 -> 0.31 s.(equal? 0.0 -0.0)is now #f, as R7RS
says (equal?on numbers iseqv?); it answered #t. - AOT: bignums 2.3-3.5x faster. Division and integer square root are the
VM's algorithms (shared code now), and a bignum is one allocation instead
of two;pi0.56 -> 0.18 s,chudnovsky0.30 -> 0.14 s, level with the
interpreter. - AOT:
readfrom a file stops asking the file once it is exhausted.
Each call after the end made a system call that returned nothing;sum1
(100,000 numbers read 25 times) 1.46 -> 0.20 s. - AOT: a program's top level runs in order. Every top-level
define
ran before any top-level expression, so a define after aset!or a
string-set!saw the old value; compiled programs now run their forms as
written, as the interpreter does. - AOT: strings are one byte a character while every character is below
U+0100, four after -- the VM's representation, and its code.string
0.45 -> 0.11 s (faster than the VM's 0.15 s),tail-58%,bv2string
-25%, and text takes the same memory under both engines. Invalid UTF-8
read into a string decodes as the VM's does: a byte that starts no valid
sequence is the Latin-1 character it would be (it used to make the whole
string bytes-as-characters). - VM:
string-fill!andstring-copy!accept any character into a
string that held only Latin-1 ones; they refused with "a wide character
into a narrow string" (string-set!already widened). - AOT: large objects reuse their memory. Strings, vectors and
bytevectors over 2 KB were a fresh OS mapping each, zero-filled by the
kernel and unmapped when collected; freed blocks are now kept by size class
and reused, bounded by the heap's own size.string2.4x faster,array1
-19%, peak memory unchanged. - AOT: tail calls between compiled functions are jumps, and every
compiled function and closure takes its arguments in registers. A tail
call no longer grows the machine stack until it bounces: continuation-
passing code runs at the VM's speed (cpstak 16.8 -> 7.2 s), and closure-
heavy code gains broadly: conform -22%, lattice -16%, scheme -24%,
graphs -21% over the three steps (closure entries in registers, known
functions' arguments in registers, jumps); the last two steps alone are
about -3% geomean over all 55 runnable benchmarks. - Embedding:
lamp.native, host functions as plain Zig functions with
typed parameters, in a library:native.Library(.{ .name = "(my lib)", .Context = Engine, .natives = .{ .{ "name", zigFn }, ... } })and
Session.addLibrary(lib, &ctx). lamp reads each argument into its
parameter's type (integers including bignums that fit, floats, booleans,
strings, symbols, bytevectors, optional and rest arguments) and boxes the
result (lists from slices); refusals name the native and the argument.
A library's names are hidden until a unit imports it, and a session not
given it cannot (docs/EMBEDDING.md). Every native is in a library: the
oldSession.defineNative,defineTypeandembed.*native helpers are
gone. - Native value types in
lamp.native:native.Type(Vec3, .{ .name, .format, .equal, .eqv, .add/.sub/.mul/.div })in a library's.types;
natives take and returnVec3; a native type is a value, so*Vec3is a
compile error. Operator overloads are typed functions, the first whose
parameters the operands fit (vec3 * number,number * vec3). lamp.nativein AOT binaries: the same library declaration compiles
ahead of time.addSchemeExecutable's.native_types(a hand-written
.zonandpack_T/unpack_Thelpers) is replaced by.native_library, a
module whose root declarespub const libraries; the build generates the
descriptor and the bridge from it. Imports of a host library are enforced
as in the VM;+ - * /on native values the compiler could not prove
dispatch at run time; an unproven argument of the wrong type raises the
VM's error instead of undefined behaviour. A native type's.format,
.equaland.eqvwork in AOT binaries too (they printed
#<native-type-0>, andequal?was identity). AOT native values hold at
most 24 bytes; a larger type is a compile error there.lamp.nativeresults: an array, or a struct or tuple, is a list of its
elements or fields --[2]usizeis(3 14), a slice ofstruct { block, start, end }a list of(block start end).- A host native called with the wrong number of arguments now says so --
"f: expected 1 argument, got 0" -- instead of "unbound variable: f". - Errors are never printed by the VM. An unhandled
errororraise
used to writeerror: <message>to standard output and fault with "a
raise with no handler"; the message is now the fault's own text --
Session.errorMessagereturns it, andlampprints it on stderr as
before. Standard output stays the program's, which a host may be using.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- AOT: values travel in registers. A value is passed to and returned
-
released this
2026-10-06 12:58:37 +02:00 | 328 commits to main since this releaseThe first tagged release. lamp is a Scheme for embedding in Zig programs,
with two ways to run the same source:- The VM (
lamp,lamp.Session): a register VM with an aarch64 JIT on
macOS on Apple Silicon. Elsewhere it interprets, which includes Linux; the
JIT is not ported there yet. Script runner, REPL with
breakpoints, and a Debug Adapter Protocol server (lamp --dap). - The AOT compiler: Scheme to Zig to native code through LLVM, with no
interpreter in the binary.addSchemeExecutableinbuild.zig
cross-compiles like any Zig program.eval, environments and runtime
loadare refused at compile time. - R7RS-small: VM 1203/1203, AOT 1198/1198 (the AOT omits
eval).
R5RS: VM 316/316, AOT 314/314 (again theevaltests). The two engines
agree on a differential corpus. - The numeric tower: fixnums that promote to bignums, and exact rationals.
No complex numbers. - Also: hygienic
syntax-rules,call/ccanddynamic-wind, proper
tail calls, full Unicode 16.0, R7RS libraries (define-library, a library
search path), and docstrings. - Embedding: several sessions per process, Zig functions as procedures,
sandboxed library sets, and native value types that compile to unboxed Zig
structs. - Performance (docs/BENCHMARKS.md, 55 R7RS benchmarks on an M1 Pro): the
VM is 0.68x Guile 3.0 and 1.84x Chez 10.4. The AOT is 1.22x Chez and 0.45x
Guile.sumandcpstakare slower under the AOT than before a 2026-09-30
correctness fix; the guarded fixnum clone that recovers them is the next
piece of work. - Platforms: macOS on Apple Silicon (with the JIT) and Linux on aarch64
(interpreted; tested in CI from this release on). Requires Zig 0.17.0.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- The VM (