Loment
EN

System · in progress

Loment VM

A second way to run a program. The binary can be handed to an executor that adopts it on demand and decides whether to adopt it by reading what the binary already carries. It is not a faster path, and it is not a way to write programs more easily. A working interpreter now exists in the repository — the package does not ship it, and the setting does not change what the compiler emits.

What exists, and what does not

The repository now carries a working interpreter tier, T0 through T3, reading the same intermediate form the linker does. T0 executes that form directly; T1 records a feedback profile without changing a byte of output; T2 decodes instruction by instruction ahead of the execution loop and runs 6.1× T0 on the corpus with byte-identical output; the compact serialisation is 28.5% of the text form for the same unit; T3 adds guarded specialisation and a way back.

What does not exist is any of that in the package. The interpreter is the repository's tools/lvm.py family, written in Python. It is not among the binaries a release ships, so an installed toolchain cannot run a program this way. Nor does the setting change the artifact: choose vm and no_vm produce byte-identical output today.

A debt the project has carried before. The runtime setting was declared in the binary for a while before it did anything, and was later brought up to what it claimed. This is that shape again: the engine exists, the shipping does not. The page says which half is which rather than leaving a reader to find out.

What it is not

Three readings are wrong, and each is one a reader arrives with. They come before the design, because saying them wrong turns this into something that tries to do everything and does none of it well.

Not the fast path

Backwards. Loment's native path already trades speed for independence, and it is not fast to begin with: against C -O2 on the array benchmark in docs/212 it runs 10.7× behind — a recorded measurement, not a standing promise. A VM is a layer on top of that. The axis of this line is not performance, and the escape hatch is a different line entirely.

Not write once, run anywhere

That job was given to compile time long ago: one IR, three outputs — Linux ELF, Windows PE, or a freestanding object. A bytecode format is no closer to portability than that, and the claim fails its own test: if portability were the point, the criterion would be "adding a target needs no new back end", and there are two targets.

Not a way to write programs faster

That is a different set of things and this touches none of them: the six other surface syntaxes, the formatter, the language server, the compile-time syntax definitions, the guide written for coding agents. LVM arrives after the program is written.

The design, in three parts

The bytecode is not a new invention

It is a serialisation of the intermediate form the compiler already emits. The bundled back end is a stack machine — one stack slot per value, arguments on the stack, aggregates through a hidden result pointer — so a complete execution model for that subset already exists and is already exercised. What is missing is the step that executes it. LVM would be a fourth reader of the same form: no new front end, no new intermediate form, no new type system.

A fifth setting

choose already had four — which layer of the library the program may use, how it reclaims memory, whether the binary may carry a runtime, and whether it may reach outside itself. vm is the fifth, and it answers which execution model is this program for? Left unwritten it is no_vm, which is what everything was before the setting existed. A release build accepts it as of 0.1.4.6; what that buys today is a declaration, not a different artifact.

The value says whether, never which. That is the rule the runtime setting already follows: what sits inside will change over the years, and a name that encoded today's rung would have to be renamed to add tomorrow's.

The gate, and why it opened

Until 0.1.4.6, a release build refused choose vm at compile time; only the development channel accepted it. The rule was blunt — until the first usable engine exists, every call to it is illegal in a release — so that an installed toolchain can never accept a promise it cannot keep. A promise that falls through is worse than a refusal.

0.1.4.6 opens it, because the refusal's premise stopped being true: the first usable engine arrived. The setting is a declaration in both channels now, and the correction is recorded rather than deleted — the check that used to demand a refusal now demands acceptance, so "this gate existed, and here is when it went" stays visible.

The gate needs a notion the repository did not have: is this compiler a release build, or a development one? The design keeps it to one constant, one value per side, flipped at packaging time. One line is deliberate — the packaged language guide is copied into a distribution unmodified, so that "the copy in the package is the copy in the repository" stays checkable, and one line is the smallest way to bend that rule. The cost is stated with it: the shipped compiler seed would no longer equal the repository's byte for byte, so the criterion drops from identical to reproducibly equal, and the difference is exactly that one constant.

The ladder

Read the VM as rungs, not as one thing to build. Each rung has to be beatable and each has to be falsifiable — "write a JIT" is not a rung, because nothing could disprove it. The parts list is taken from two systems that have done this at scale; the implementation would be the project's own, and whether it stays that way is an open decision.

T0 — bytecode and a direct interpreter

The floor. Built in the repository. Criterion: a program run this way produces byte-identical output and the same exit code as the native artifact.

T1 — feedback

Profiles of types, branches and call sites. Built in the repository. Criterion: the profile lands in the artifact and a third party can read it — and switching it off leaves the artifact byte-identical.

T2 — a baseline compiler

Removes the cost of interpretation without optimising. Built in the repository — 6.1× T0 on the corpus. Criterion: measurably faster than T0, semantically identical.

T3 — an optimising compiler with a way back

A guess that turns out wrong has to be given up. Built in the repository, for the one optimisation that carries a guess. That is the only hard criterion at this rung: after a guess is abandoned, the program's result matches the native artifact.

T4 — generational and concurrent reclamation

The one rung that is not an extension of the others. The reclamation design used to say this could not be done here at all: the back end kept pointers only in locals and parameter slots, and that rules out the usual write barrier. That reason no longer holds — a way to turn a pointer into an integer arrived, and the generational half landed on the reclamation side, with a card table and a write barrier. What is still ahead is doing it inside the VM, which is a different problem from doing it ahead of time.

The repository carries T0 through T3; the package carries none of them. Building the interpreter and shipping it in the distribution are two different steps, and only the first is done. The rungs above are the shape of the plan, not a schedule.

What it is for

Cold water first: the two things large projects most often want — build faster, run faster — are both out of scope. Build time is a problem inside the compiler, and run time is the wrong axis for this line.

But "large" has already exposed one problem that is structural rather than a defect, and this is the only thing that can cure it. Emitted symbols are flat: a hundred modules means the function names have to be made globally unique by hand, and two modules that each define the same small name are refused. That is a consequence of "one static link" as a model — the classic scalability wall of a large project, pushed directly into the user's face by the language's own structure. The repository already runs an oversized-project corpus as a criterion, so this is measured rather than guessed.

Adopting a module at run time

Adding a module to a native artifact means relinking the whole program. Here it means loading a unit.

Replacing one without relinking

Change a module, reload it, and leave the rest of the program where it is.

Running code you did not write

A third-party extension, or one a machine wrote. This is where the project's own ideas first have a job to do. At link time the port table, the ranges a unit declares it may reach, the record of what it does not have, and its reclamation numbers are audit — something to read afterwards. At load time those same records become a gate — something to decide with, before anything runs.

Which is the one sentence this page will stand behind, and it is falsifiable: adding an extension module does not mean relinking the whole program — and before it is loaded, you can tell whether it can reach what it should not.

What it does not claim

The design record keeps this list, and it belongs on the page rather than in it.

That it is new

Nothing in it is a new brick. Execution evidence carried in the artifact and judged at load time is from the 1990s; refusing to load what cannot be proved is how sandboxed runtimes work; one input with several execution models is what the largest mobile runtime does. What might be placed differently is the seam, not the parts.

That nobody has done those two things

The wording throughout is not found in a search, which is not the same as not done. Two searches that would kill the claim are written down as open criteria, with the honest note that they were not run exhaustively.

That it is useful

The box is empty today. And the third role above rests on a number nobody has measured — how much of that oversized corpus is really code that should have been loaded late. If the number comes out near zero, the role is a nice story.