Changelog
Format: Keep a Changelog. Version scheme: semver.
Prior release numbers (0.01.0 → 0.32.0) tracked internal phase milestones during the pre-1.0 implementation; they are retained below as historical record. The first public version is 0.1.0.
No versioned release is tagged yet, but the
rolling dev release
rebuilds the full six-platform matrix every day from the tip of
main. The [Unreleased] entries below therefore describe what is
already runnable in the latest dev build — grab one to try changes
before 0.1.0 is cut.
[Unreleased]
Fixed — iterator element types resolve in a project, not just in a file (2026-08-27)
The same file gave two verdicts depending on how you asked:
mount core.base.iterator.{Transducer};
fn pick(xs: &List<Int>) -> List<Int> {
let t: Transducer<Int, Int> = Transducer.filter(|x| x > 0);
xs.iter().transduce(t).collect()
}
verum check pick.vr clean
verum check (project) error<E400>: Type mismatch:
expected 'Item<ListIter<Int>>', found 'Int'
verum run (project) prints the right answer
Item<ListIter<Int>> IS Int. The projection reached the comparison
unreduced, so the mismatch was reported against a type nobody wrote —
and only in project mode, which is the mode that gates a build.
The resolver was never the problem: it was called and answered correctly
in the same run that reported the error. The comparison simply did not
ask it. check_expr now reduces the expected type through the same
helper that match scrutinees already used, so every caller gets it.
If you have been seeing expected 'Item<…>' on map, filter, fold
or a transducer chain in a multi-module project — and finding that the
same code checks clean as a single file — that is this, and it is gone.
Fixed — a match over your own type proves what its arms return (2026-08-27)
The most ordinary function a sum type has is "map the case to a number" — a rank, a priority, a severity. It could not discharge a refinement on its own return type:
public type Standing is Trusted | Suspended(Text) | Revoked(Text);
public pure fn severity(s: &Standing) -> Int{it >= 0} {
match s {
Standing.Trusted => 0,
Standing.Suspended(_) => 1,
Standing.Revoked(_) => 2,
}
}
✗ severity: Failed
Counterexample: result = (- 1)
result = (- 1) is the prover reporting that the body told it nothing.
A function's tail expression is turned into a VALUE and substituted for
result; the translator that builds that value handled if and not
match, so a match in tail position produced a term nothing
constrained. Anything stated about such a function inherited the silence
— which is why a theorem over a three-case analysis could sit unproved
with no visible reason.
Written over a Bool the same body proved from the start, because those
arms are literal patterns the translator did handle. If you have been
avoiding refinements on match-bodied functions, they work now.
Fixed — an affine value may be consumed on a path that returns (2026-08-27)
An affine type promises the value is used at most once. The check asked a different question — whether a loop happened to be around it:
fn run(t: Token, n: Int) -> Int {
let mut i = 0;
loop {
if i >= n { return take(t); } // consumes, then leaves
i = i + 1;
}
}
error<E302>: affine value `t` cannot be used in loop
The consuming path returns, so no execution reaches a second
consumption. The rule is now about whether the loop's back-edge is
reachable from the consumption, not about whether a loop encloses it: a
block whose own top level carries an unconditional return cannot come
back, so consuming there is allowed.
Still refused, deliberately: a consumption the loop CAN come back from,
and one that only breaks — break leaves the innermost loop, so under
nesting an outer loop can still come round. Both poles ship as
conformance specs, because a relaxation of a soundness rule and a
removal of it look identical from the passing side.
Fixed — a complete match over an error type from another module is accepted (2026-08-27)
A module declares an error type; another module returns it inside a
Result; the caller matches every constructor. That was reported as
non-exhaustive whenever the error type came from a sibling module and
any constructor carried a generic payload:
// lib/first.vr
public type LoadError is
| Missing
| Ambiguous(List<Text>);
public fn load(name: Text) -> Result<Config, LoadError> { … }
// main.vr
match load("app") {
Ok(cfg) => run(cfg),
Err(LoadError.Missing) => print("no such config"),
Err(LoadError.Ambiguous(xs)) => print(f"{xs.len()} candidates"),
}
error<E0601>: non-exhaustive patterns: `Err(_)` not covered
The match covers everything, and the workaround people reach for —
adding Err(_) => … — is exactly what exhaustiveness exists to make
unnecessary: a catch-all silently absorbs the constructor somebody adds
next year.
Underneath, a generic type's parameters were recorded by NAME with a
placeholder standing in for each parameter itself, so Result<Config, LoadError> substituted its first argument and left the second
untouched. The type's own variables are now recorded, and there is one
of each per type rather than one per constructor — which also means a
type like type Pair<T> is Both(T, T) | One(T) now forces all three of
its payloads to agree.
Fixed — Maybe.Some(x) binds x to the same type Some(x) does (2026-08-27)
The two spellings of a variant pattern are meant to be the same pattern. The qualified one lost the payload's type:
fn name_length(m: Maybe<Text>) -> Int {
let s = match m {
Maybe.Some(t) => t,
Maybe.None => return 0,
};
s.len()
}
error<E404>: Ambiguous type for `s`: the inferred type `_` is not fully
determined by this function
Spelled Some(t), the same function compiled. The qualified form
rebuilt the variant from the qualifier's NAME — a template whose payload
is an unbound variable — instead of using the scrutinee's own
Maybe<Text>, which already knew the answer.
It stayed hidden because it usually did not matter: any sibling arm that
yields a value pins the variable anyway. Only when every other arm
DIVERGES — return, panic() — is there nothing left to determine the
binding, which is why the shape above is the one that fails.
One consequence is worth knowing about when you upgrade. Because the payload now binds to its real type, code that dereferenced such a binding is reported instead of accepted:
match &limits.max_age {
Maybe.Some(n) => Maybe.Some(*n), // error<E409>: cannot dereference
Maybe.None => Maybe.None,
}
Matching a reference auto-dereferences the scrutinee and binds payloads
BY VALUE, so n is a value and the * was never right — it was
accepted only because the binding used to have no type to check it
against. Drop the *.
Fixed — a refinement on a compile-time parameter is enforced (2026-08-27)
A meta parameter could carry a refinement, and nothing checked it:
public type Hash<N: meta USize { it == 32 || it == 64 }> is {
bytes: [Byte; N],
};
let sha1: Hash<20> = …; // accepted, and it ran
The grammar admitted the predicate, the parser stored it, and every reader of the parameter dropped it — a feature present, accepted, documented, and doing nothing, so anything built on it inherited a guarantee that was never made. It now reports:
error<E506>: meta argument 1 of `Hash` violates its refinement:
`20` does not satisfy `it == 32 || it == 64`
The other half already worked and is unchanged: the width is part of
the type's identity, so Hash<32> and Hash<64> are different types
and a function taking one cannot be handed the other.
Only arguments that evaluate at compile time are judged — a parameter passed through from an enclosing generic has no value yet, and nothing is concluded about it. So code that was correct stays accepted; what changes is that code claiming a guarantee now gets it.
Added — predicates over your own types are provable (2026-08-27)
@verify(formal) could prove claims about predicates over Int and
little else. Four things landed together, and each was a claim that
could not be STATED rather than one that failed to prove — a
distinction that matters because the two print almost the same.
Records and their fields.
public type Box is { a: Int, b: Int };
public fn fields_commute(x: Box) -> Bool {
x.a + x.b == x.b + x.a
}
@verify(formal)
public theorem holds(x: Box)
ensures fields_commute(x)
proof by auto; // now closes
Variants, as parameters and as values. A match over an enum
reflects, so is_red(Colour.Red) unfolds to its body; a variant-typed
parameter can be spelled at all. A Bool parameter used to abort the
compiler with an internal error where a proof belonged.
Types your module mounts. The shapes now come from every module the check loaded, not only the file being verified — so a type declared in a sibling file works exactly like one declared locally.
Early-return guards. A comparison over more than one component is written one way by everyone:
public fn older(a: &Version, b: &Version) -> Bool {
if a.major != b.major { return a.major < b.major; }
if a.minor != b.minor { return a.minor < b.minor; }
a.patch < b.patch
}
That shape was outside the reflector, so nothing about such a predicate
could be proved — not even that nothing is older than itself. It folds
into nested if … else now, alongside the let folding that landed
earlier.
Nothing became provable that should not be: each of these ships with a soundness spec where the FALSE version of the same claim must still fail, including one that fails only because guards are applied in source order.
The verifier also says more when a goal does not close. It names what
disagreed (argument 4 is Int, declared "Verum!AuthorityRole"), and
the verdict that used to read "SMT: goal is unsatisfiable" when the
solver had found a COUNTEREXAMPLE now says so.
Changed — a call through a module path is checked (2026-08-27)
A call whose receiver is a module path of three or more segments was not resolved, and its argument count was not checked at any path length. Both now are.
module a {
public module b {
public fn g(x: Int) -> Int { x + 1 }
}
}
a.b.absent() // now: error<E100>: unbound variable: a.b.absent
a.zzz.g(1) // now: error<E100>: unbound variable: a.zzz.g
a.b.g() // now: error: `g` expects 1 argument(s), but 0 were provided
The third line is the one worth pausing on. It used to check clean and
run, answering 1 — the missing argument read as unset memory.
Everything in the neighbourhood was already checked, which is why this
went unnoticed for so long: a two-segment path reported a missing leaf,
a missing method on a value reported a type error, a missing field
reported a field error. Only the module-path form was exempt, and its
symptom was a call that quietly did nothing rather than one that failed.
Working code is unaffected — chained calls
(base64.decode_url(s).map_err(f)), constructors reached through a
module path (some.module.Colour.Red), methods on associated
constants (MemProt.READ_WRITE.to_unix_flags()) and nested module
calls all resolve exactly as before.
Eight dead calls inside the standard library came out with the change,
each repointed at the callee it meant. Two of them mattered beyond the
call site: Ulid.generate() was filling its random seed through a
call that did nothing, so every ULID it minted carried a zero seed;
and a QUIC server's accept loop spawned nothing, dropping every
connection it accepted.
If your own code now reports E100 on a module-path call, the call was
not reaching a function before either — it was evaluating to nothing.
Fixed — a file path held in a record field names the right file (2026-08-27)
Writing to a path kept in a struct field reported success and produced no file:
type Store is { path: Text };
let s = Store { path: "/tmp/out.txt" };
core.io.file.write(&s.path, &"x") // Ok(()) …and no /tmp/out.txt
What it produced instead was a file named, literally,
<ptr@0x9d4831cf8> in the working directory, and a later read through
the same field returned that file's contents — so the round-trip agreed
with itself and the program had no way to notice. A store that keeps
its own path in a field, which is where any store keeps it, lost
everything it held and reported success in both directions.
&record.field is a different runtime shape from &local: it is an
interior pointer to the field's slot, and the file-I/O layer's argument
handling knew the other two shapes but not that one. It now reads the
one authority every &T argument goes through.
The second half of the fix is that a value which is not a path no longer becomes one. The old fallback rendered the value as text on the reasoning that a visible wrong path beats a confusing "not found" — but a rendering is a perfectly good filename, so the loud failure became a silent write to the wrong place. Anything unrecognised is now refused, which is also why this now behaves:
core.io.file.write(&s.path, &"x") // path in a missing directory
// now: Err(NotFound)
Fixed — a predicate proves over the type it was written for (2026-08-27)
@verify(formal) could prove claims about predicates over Int and
essentially nothing else. A predicate taking a Bool, a Text or a
record was unprovable however trivial:
public type Box is { a: Int, b: Int };
public fn fields_commute(x: Box) -> Bool {
x.a + x.b == x.b + x.a
}
@verify(formal)
public theorem holds(x: Box)
ensures fields_commute(x)
proof by auto; // would not close
A user function reaches the solver as a declared symbol plus a defining axiom, and the two were written by different code that disagreed about what sort a parameter has — the solver saw two unrelated symbols that happened to share a name, so the axiom never bore on the goal. There was no error and no diagnostic, only a theorem that would not close.
A Bool parameter was worse than unprovable: it aborted the
compiler with an internal error where a proof belonged. Both sides
now read the same authority for a parameter's sort, and an argument
that does not carry the declared sort is a named refusal rather than a
crash.
Fixed — a reference that outlives what it points at is refused (2026-08-26)
A &T held in a record field and read after its referent's scope had
ended returned whatever was in that memory:
type Keeper is { held: &Big };
let mut k = Keeper { held: &Big { a: 1 } };
{ let v = Big { a: 7 }; k = Keeper { held: &v }; }
k.held.a // printed Variant(196608)
The value depended on what had been allocated since — one run produced an interned string from elsewhere in the heap — which makes it a memory-safety defect rather than a wrong answer. Nothing was reported at compile time either.
&T is the default tier, the one whose entire purpose is that this
cannot happen. Nothing was missing from the machinery: references carry
a generation, the counter advances when a scope ends, and the check
compares them. It was simply not being called on the path a field
read takes, while the explicit dereference had always called it — so
which of two spellings a program used decided whether it was safe.
Reading through a stale reference now stops the program:
error: CBGR use-after-free detected: expected generation 2, found 3
A reference used while its referent is alive is unaffected, and so is every value that is not a reference — the check runs only where there is something to check, which is the tier's documented cost.
Fixed — a context works across module boundaries (2026-08-26)
A context declared in one module could not be used from another, which
left dependency injection working only where it is not needed — a
context declared in the file that consumes it is a dependency nobody had
to inject.
// src/config.vr
public context Clock { fn now() -> Int; }
// src/service.vr
mount demo.config.{Clock}; // error: cannot find `Clock`
Two things were in the way. The context was missing from the module's
export table, so the mount could not find it; and once it could, the
provide still reported undefined context, because the import
told the context resolver about it and never told the context checker
— which is what provide consults.
Both are fixed, including a function depending on several contexts from the same module.
module { … } block is still affectedA context declared inside an inline block in the same file is not yet found. Contexts that cross a file boundary — the usual arrangement — work.
Fixed — a future survives a collection, and a type annotation (2026-08-26)
Fanning out — spawn several tasks, keep the handles, await them — is what structured concurrency is for, and two of its spellings answered with garbage instead of an error:
let mut handles: List<Future<Int>> = List.new();
handles.push(spawn(async { work(1).await }));
handles[0].await // a large negative integer, not the result
let h: Future<Int> = spawn(async { work(1).await });
h.await // the same
The second is the one to know about: writing the type down broke the
program. Without the annotation the compiler recorded the handle's own
type and awaited it; with the annotation it saw Future<Int>, did not
recognise a handle, and passed the raw task marker through as if it were
the answer.
Both now work, as does for h in handles { … h.await }.
handles.iter() is still refused, and deliberately: awaiting consumes
a future, so a reference to one is not awaitable. The by-value for
above is the spelling that consumes.
Fixed — a refinement on a variant payload is enforced (2026-08-26)
type Audit is
| Consistent(Int{it >= 0})
| Contradicts(Text);
Audit.Consistent(-1) // built happily, printed "over -1"
The same refinement on a record field or a return type stopped the program with a precise message. On a variant payload it was decoration — and nothing at the declaration said which positions are which, so it read as a guarantee the compiler had never made.
It now answers in whichever way is available earliest: a value the
compiler can see is refused at compile time with E500, and one that
only exists at run time raises refinement violation: field Audit.Consistent.0``. A same-named variant of a different type is
unaffected — the check is tied to the type that declared it.
Still inert, and worth knowing before you rely on it: a refinement
reached through a generic argument, as in
Result<Int{it >= 0}, E>. There the refinement comes from instantiating
Result<T, E>, which declares none of its own.
Fixed — a project with more than one file runs (2026-08-26)
verum check reported no errors and verum run refused the same
sources with E402: module 'demo.helper' not found — so a project
type-checked but could not be executed the moment it was split across
files.
Three separate causes were in the way, each hidden behind the one in front of it:
- the module root came from the directory name rather than the cog
name in
verum.toml, so a checkout in a directory calledwork-copyregisteredwork_copy.helperfor a file that declaresmodule demo.helper;; - a
src/directory became a module segment, so the same file was registered asdemo.src.helper; - the entry-point search accepted a method named
main, so a program with exactly one entry point was refused as ambiguous.
Neither of the first two was visible in the standard library, whose
directory name matches its cog name and which has no src/.
Fixed — iterating a slice, and methods on a wrapper that dereferences (2026-08-26)
for x in items[1..] failed to find iter. A slice's type is spelled
[T], and method lookup was searching for a type by that spelling
instead of the Slice the methods are declared on.
Separately, a method called on a wrapper type that implements deref
now reaches the wrapped value instead of reporting the method missing.
The call is retried once against the dereferenced receiver — the same
step a reader performs when they write handle.len() and expect the
handle to stand in for what it holds.
Fixed — a type you declare keeps its own arity (2026-08-26)
Declaring a type whose name matches a standard-library generic produced a program refused against itself:
type Slice is { count: Int };
fn build() -> Slice { Slice { count: 3 } }
// error<E400>: Type mismatch: expected 'Slice', found 'Slice<_>'
The record literal took its fields from your declaration and its
parameter count from the standard library's Slice<T>, then
instantiated a type variable for a parameter your type does not have.
One name, half a question answered by each of two types.
The cause was silence rather than a wrong answer: a declaration with no type parameters recorded nothing, and whatever a same-named type had recorded earlier kept answering. Every declaration now states its own arity, "none" included.
Fixed — return match { … } type-checks like the tail form (2026-08-26)
Returning a match explicitly reported errors that the identical code
without return did not:
fn parse(b: Bool) -> Result<Int, Text> {
return match b {
true => Ok(1),
false => Err("bad"), // reported: expected 'Int', found 'Text'
};
}
The returned value was being typed from the bottom up, with nothing to compare it against, so the match's arms were unified with each other instead of with the declared return type. Return values are now checked against the signature, and the two spellings behave the same.
Fixed — ? works on a protocol method that returns a Result (2026-08-26)
self.field.fmt_debug(f)? — the ordinary way to write a Debug
implementation that delegates to a field — reported "Result does not
implement Try", even though the method is declared to return
Result<(), FormatError>.
Two things had to be fixed for it. The standard library's protocol
descriptions were arriving with no methods at all, because a
protocol re-exported from several modules was overwritten by an empty
entry. And a method's recorded return type was dropping its type
arguments, so Result<(), FormatError> became a bare Result that ?
could not recognise.
Changed — BufReader's line reader is named next_line (2026-08-26)
BufReader had its own read_line() alongside the BufRead protocol's
read_line(&mut Text). Two methods of the same name on one type made
every call ambiguous, and reading a file line by line silently produced
empty lines. The convenience method — the one that answers
Maybe<Text> and reports end-of-file as None — is now next_line
(and next_line_async). The protocol's read_line is unchanged.
Fixed — the playground's screen, its VBC lens, and headless replay (2026-08-25)
- Nothing can paint over the interface any more. Diagnostics from the compiler, warnings, and messages from background work used to go to the same terminal the notebook draws on, smearing borders and panels — which looks exactly like the interface breaking. Those writes are captured and shown in a new Console lens instead.
- The VBC lens no longer freezes the frame. It compiles the whole
notebook, which was happening on the interface thread; cycling
through the sidebar felt like a hang. It now works in the
background and shows
disassembling…until its answer arrives. - A cell that prints nothing says so. An empty output panel and a broken one used to look identical; a silent run is now labelled, and the status line reports how many output lines arrived.
verum play --replayworks. Headless replay never installed the script compiler and answeredCompilerUnavailablefor every book — reading as a broken book rather than a missing step.
Fixed — Ok(…) and Some(…) work in multi-file projects (2026-08-25)
Writing Ok(value) or Some(x) without naming the type worked when a
file was checked on its own, and failed with not a function: Ok
when the very same file was part of a project. Since a project is how
every real program is built, the idiomatic spelling was effectively
unavailable outside single-file scripts, and the workaround —
qualifying every constructor as Result.Ok(value) — spread through
codebases as though it were the required form.
Both ways of building now register the standard library's variant constructors identically. The qualified spelling remains valid; it is no longer forced.
Changed — an unmet architectural obligation is an audit finding, not a build failure (2026-08-25)
Architectural declarations distinguish two things that used to be punished alike:
- A false claim — code declaring a property it does not have — remains a compile error. The declaration is wrong, and that is a defect of the same order as a type error.
- An unmet obligation — a promise that is genuine but not yet
discharged, such as a lifecycle stratum awaiting its closure —
is now a warning during compilation and a finding in
verum arch check. Running the audit with--strictstill exits non-zero, so a pipeline that gates on it keeps gating.
Previously a single outstanding obligation made a source file uncompilable, which also removed its names from every module that mounted it — one unfinished promise could look like a dozen unrelated failures elsewhere.
Fixed — a shared value reports its own length (2026-08-25)
Shared.new(list).len() answered 2 — for every list, whatever its
length. A shared handle is a small cell holding a reference count and
the value, and the length was being taken from the cell rather than
from the value inside it. The number was plausible, nothing reported
it, and Shared is the ordinary way to share a value.
len and is_empty now read through to the value, as field access and
method calls already did. strong_count still reports on the handle,
which is what it is for.
Indexing a shared list (shared[0]) is still not resolved and reports
an ambiguous type; that is a separate gap and is being worked.
Fixed — a qualified path now costs what a mount costs (2026-08-25)
Calling something through its full path — core.time.Duration.from_secs(30),
the documented way to use a name once without mounting it — produced the
same program and the same answer as the mounted spelling and took
6 minutes 19 seconds to compile where the mount took under a second.
On anything with a timeout that reads as a hang.
The compiler was reading a dotted expression one link at a time and
treating each link as a name to go looking for. Since every standard
library module mentions core, looking for a function called core
meant reading nearly the whole library: 582 modules and 48,815 functions
loaded to call one function. A dotted path is now read as a single name.
Both spellings now load exactly the same 11 modules, and the one-line program compiles in seconds. Choose between them on readability; compile time no longer argues for one over the other.
Fixed — a method's reference tier no longer decides whether it is a method (2026-08-25)
A method declared with a tiered receiver — fn read(&checked self) or
fn read(&unsafe self) — was filed as an associated function. The
symptom was asymmetric and therefore confusing: Counter.read(&c)
worked while c.read(), the way every call site actually spells it,
reported "no method named read". The zero-overhead reference tiers
were consequently unusable on methods, which is most of the code that
would want them. See
references → tiers as method receivers.
Fixed — refinement types no longer hide what the underlying type can do (2026-08-25)
Indexing a value whose type carried both a reference and a refinement,
as in fn nth(xs: &List<Int>{len > 0}, i: Int) -> Int { xs[i] }, was
rejected with "cannot index non-indexable type". Either layer alone
was fine; only the combination failed. A refinement says which values
inhabit a type — it never removes an operation the base type supports,
and indexing now sees through it.
Fixed — Int.MAX and friends are known to the verifier (2026-08-25)
A function promising a refined result and returning a limit constant —
fn abs_value(n: Int) -> Int{>= 0} { … Int.MAX … } — was reported as
violating its own refinement, with a counterexample that named an
impossible value. The verifier treated Int.MAX as an unknown
quantity, so the solver was free to imagine a negative one.
Limit constants of the primitive numeric types (MIN, MAX, BITS,
and the float constants EPSILON, PI, E, MIN_POSITIVE) now
carry their true values into verification, and proofs that depend on
them go through.
Changed — the compiler refuses to ship a degraded build (2026-08-25)
Compilation is now strict by default. Where the compiler previously fell back quietly and kept going — monomorphisation dropping to the unspecialised module, a signature collision leaving a function declared without its body — the build stops and the message names the site. Those fallbacks produced binaries that link and then behave oddly, which is the most expensive kind of failure to chase.
The permissive behaviour is still available, now as a deliberate
choice: verum build --lenient. See
build system → degradation is a build error.
Fixed — generators and iterator chains: eager consumption is now trustworthy (2026-08-24)
gen{ … }values survive every way of consuming them. A generator expression driven by aforloop, collected with.collect(), returned from a function asGenerator<T>, or hidden behind an existential-> some I: Iterator<Item = T>now yields the same elements on both the interpreter and AOT tiers. Previously each consumption path failed in its own way — silently empty sequences, a list whose.len()crashed, or a program that exited mid-mainon the first iteration — and the documentation'swindow_pairsexample (Comprehensions → Generators) did not run at all. It runs now, exactly as printed.- Capturing generator bodies work under AOT. A generator that
closes over locals (
gen{ i * i for i in 0..n }inside a function takingn) compiled to a body that ignored its captures on the native tier, iterating an empty range. Captures now arrive correctly for up to six captured values. - Adapter-chain methods resolve wherever the chain was built.
Long lazy chains (
.iter().map(..).dedup(),.inspect(..) .map(..),.peekable()thenpeek_mut()) hit "method not found" or mis-typed closures when the receiver type was known only through a return value. The archive loader now tracks return-type reachability, so an adapter's whole method surface loads with it — cold-start cost is unchanged. partitionbuilds its containers. A terminator whose result container appears only in the return type (partition(p) -> (C, C)) received no type witness at runtime and fabricated an integer where aListbelonged. The witness now flows from the binding annotation:let (evens, odds): (List<Int>, List<Int>) = range(0, 10).partition(|x| *x % 2 == 0);splits correctly.extendaccepts iterators again.Extend.extendtakes any iterator, but the runtime shortcut behind it read whatever it was handed as an array — element access on aList, FIELD access on anything else.list.extend(range(5, 7))appended[5, 7, false](aRange's three fields), extending from a mapped iterator appended the iterator and the closure, andextend_one(x)— which the protocol defines asextend(once(x))— appended the wrapper instead of the value. Everything that builds a container element-by-element inherited it:partitionreturned wrapped elements,unzipand the grouping patterns failed outright. The shortcut now applies only to real array-shaped collections and otherwise runs the ordinary loop.- Explicit
size_hintimplementations are dispatched again. A materialised protocol default could shadow a type's own override depending on declaration order;range(0, 10).size_hint()answered(0, None)instead of(10, Some(10)). Declaration order no longer decides dispatch.
Added — the Playground reborn: gallery, lenses, replayable books (2026-08-23)
-
Cells execute through the real compiler — the notebook's parallel execution bridge is deleted; cells run as a growing module through the same stdlib-linked compiler
verum runuses. Practical difference:mount core.…works in a cell (the old bridge silently produced nothing), failures are honest compiler diagnostics, and the Vars lens shows values from the actual run. -
verum playopens a gallery on empty launch — guided tours built at compile time fromdocs/by-example(one truth of the examples; the old hardcoded tutorial decks are deleted), recent books, blank sheet. The status line always teaches the next 3–5 keys;?shows the whole map. -
Research lenses in the sidebar: Arch (the notebook's inferred capability surface, escalations red, dead rights yellow — via
verum arch query), VBC (the bytecode the interpreter actually runs), Tiers (truns the interpreter-vs-AOT judge on demand and reports its cost), Journal (every question the session asked, with wall time and chain address). -
.vrbookv2 carries a content-address chain over its code cells:verum playbook --replayrefuses an out-of-step hand edit before execution (exit 2), re-runs every cell from scratch, and names the first bit-level divergence by cell and address (exit 3);--freeze report.mdwrites the frozen snapshot of what actually happened. Execution timing is a price badge, not a result — it never diverges a replay. -
verum inspect BINARYreads the shape manifest every AOT binary now embeds — schema, toolchain, and the program's pinned + inferred capability surface — with no compiler present. -
Your First Hour (getting-started) — a guided sixty minutes from install to a formally verified function, every command output pasted from a live run.
Fixed — entry points, f-string Debug, honest math refinements (2026-08-23)
module X;+fn mainnow runs on both tiers. The header qualifiedmainintoX.main; the interpreter reported "No main function found" while the AOT binary silently exited 1. One rule now lives in the bytecode module itself: baremain, else the unique*.main, else an honest ambiguity report.f"{x:?}"works everywhere — the Debug arm desugars to a bareformat_debugcall, which the prelude now provides (the grammar itself emits the name, so the inclusion law applies).- Math return refinements state the exact IEEE-754 image.
exp: Float{> 0.0}was real analysis, not IEEE:exp(-2839)(the log-domain probability idiom) underflows to exact0.0and died on a runtime refinement assert. The whole family (exp,exp2,sin,cos,abs,cosh,tanh,sech,erf) now carries its true image — boundaries IEEE reaches are included, NaN is spelledit != it. sh("...")returns what its signature says. The Tier-0 shell intercept wrapped every entry point's result inResult;sh— whose contract is a bareShellResult— panicked on.success(). Each entry point now honours its declared contract.
Added — inference-first architecture: verum arch query, transitive capability inference, protocol @max_shape (2026-08-22)
- The compiler now COMPUTES every module's capability surface —
the ATS-V-2 inference-first discipline landed. Per-function
summaries are solved over the local call graph (SCC fixpoint), so a
helper's
Networkcapability reaches its caller's surface transitively; mounted callees resolve across module boundaries through the compilation session; calls the resolver cannot name are surfaced, never guessed. verum arch query --at FILE [--json]exposes the same inference as a tool: inferred surface (with per-atom provenance), the@arch_modulepin, and the two-direction judgment — code exceeding the pin is an escalation, a pinned right nothing exercises is a dead right. The JSON schema is append-only: it is the machine contract coding agents consume.- Protocols can declare
@max_shape(...)— an upper bound on what implementations may do. A call through a protocol-typed parameter contributes that bound as a cited fact and keeps the summary closed: bounded polymorphism at the trait seam, no silent widening. - The
@arch_modulepin parser now parses its arguments. All eightCapability.<Variant>(args)forms used to ignore their payloads ("placeholder fillers"), so every pin in the corpus parsed to the same stamped values and the escalation check could agree with reality only by accident. Caught by the new clean-twin fixture on day one; all eight arms now read their real payloads.
Added — verum diff-tiers: the tier-identity judge (2026-08-22)
One bytecode, two executions is the language's core promise. The new
command runs a program under both tiers as isolated subprocesses,
compares program output and exit codes, and returns identical or
DIVERGENT (exit 3) with the first diverging line — turning the
tier-identity guarantee from a point fix into a continuously
checkable institution. A semantic-debt ratchet accompanies it: the
count of codegen degrades (places where static resolution gave up)
is recorded per build and budgeted to only go down.
Fixed — AOT honours the allocation intrinsics' declared contract (2026-08-22)
-
cbgr_allocreturns what its signature says — on both tiers. The declaration promisesResult<(&unsafe Byte, UInt32, UInt16), AllocError>; the interpreter always built that value, but the native (AOT) lowering used to place a raw pointer in the result register. Consumers then pattern-matched the pointer as if it were aResultvariant and read garbage — the root of a family of macOS AOT crashes whose only visible frame wasShared.new. The AOT path now builds the fullResultbit-for-bit like the interpreter (validation, error variant, generation/epoch read back from the allocation header), and allocation goes through one aligned allocator instead of three drifting copies. -
Alignment is a real parameter now. The raw-memory intrinsics (
alloc,alloc_zeroed,dealloc,realloc) declared analignthe compiler read and discarded — three handlers hardcoded 8, one mapped 0 to 8. Alignment now flows from the declaration through both tiers to the actual allocation, a zero-size allocation is freed correctly instead of leaking, and freeing under a different layout than the allocation is refused loudly. -
The contract can no longer drift silently. A CI-gated test now cross-checks every
@intrinsicdeclaration's parameter count against the dispatch registry. It caught five real mismatches on landing (including two stale redeclarations that matched neither the canonical declarations nor the registry, and a 1046-line shadow signature table nothing ever read — both deleted). -
fn f(it: Int) -> Int { it.abs() }now compiles. It used to fail with "expected{to start function body", because the compiler decided "is this brace group a refinement of the return type, or the body?" by looking at the FIRST IDENTIFIER inside it —itfollowed by a dot meant refinement, any other name meant body. A parameter nameditwas therefore unusable in tail position:fn consume<I: Iterator>(it: I) -> Int {it.count() // was: parse error — now: fine}The rule is now read off the grammar instead: a function body is mandatory, so the group is a refinement exactly when the signature CONTINUES after it (the body, a
;, or a clause such asusing/where/requires/ensures). Nothing depends on identifier names or on whitespace. -
Refined return types are unaffected. Both spellings keep working:
fn sin(x: Float) -> Float{it >= -1.0 && it <= 1.0} { ... }fn is_even(n: Int) -> Bool { n % 2 == 0 }So are casts —
let y = match x as Int { 1 => 10, _ => 20 };parses as it always did.
Fixed — writes through lock guards actually land (2026-08-09)
-
*guard = valueon aMutexorRwLockguard reaches the protected data. Until this fix the write was silently lost: the lock was taken, the assignment ran, the guard released — and a later read saw the old value:mount core.sync.rwlock.{RwLock};fn main() {let l: RwLock<Int> = RwLock.new(1);{let mut w = l.write().unwrap();*w = 42;}let r = l.read().unwrap();let seen: Int = *r;print(f"{seen}"); // was: 1 — now: 42}Three compiler defects stacked to produce the silence, and all three are fixed: taking a field's address through a reference receiver was refused outright; the reborrow
&mut *(ptr as *const T as *mut T)returned a reference to a temporary copy instead of the address; and the field offset of a generic record's field fell back to 0, so the write that finally happened landed on the lock's own state word. Bothsync/mutexandsync/rwlockconformance families are green, and the pins that guard this are the operations themselves — lock, write-through, read-back.
Fixed — Mutex.lock() and try_lock() succeed at Tier-0 (2026-08-09)
-
Locking a
core.sync.mutex.Mutexworks. Until this fix everylock()returnedErrand everytry_lock()returnedTryLockError.WouldBlockon a freshly created, uncontended mutex — which also left the async task queue, the signal registry and the global tracer inert, since all three guard their state with this Mutex:mount core.sync.mutex.{Mutex};fn main() {let m: Mutex<Int> = Mutex.new(5);let guard = m.lock();print(f"{guard is Ok(_)}"); // was: false — now: true}The cause was name shadowing, not the lock itself: the platform layer's bare aliases (
public type Mutex = DarwinMutex;and the Linux twin) captured the name inside the compiler's alias table, so a method call on aMutexvalue was emitted against the platform type —m.lock()compiled toDarwinMutex.lock, which reads a different layout and fails. The colliding aliases (Mutex,Condvar,SpinLock,Onceincore.sys.{linux,darwin}.thread) had no consumers and are removed; reach platform primitives by their own names (DarwinMutex,LinuxMutex, …).
Fixed — UUID v7 IDs mint in sorted order within one millisecond (2026-08-09)
-
Uuid.new_v7()now carries the RFC 9562 §6.2 monotonic counter.rand_aused to be 12 random bits, so IDs generated inside a single millisecond came out in arbitrary order — 500 consecutive IDs showed 37 inversions, defeating the property a v7 database key is chosen for. The 12 bits now hold a counter, advanced together with the 48-bit millisecond under one compare-exchange, so the pair can never tear; a backwards clock step never lowers the stored millisecond, and counter saturation re-reads the clock instead of borrowing random bits:mount core.id.uuid.{Uuid};fn main() {let a = Uuid.new_v7().to_text();let b = Uuid.new_v7().to_text();print(f"{a < b}"); // true — even inside the same millisecond}Entropy accounting changed accordingly: 62 random bits plus a 12-bit counter, rather than 74 random bits.
Changed — ID generators live in core.id, not core.base (2026-08-09)
-
uuid,ulid,nanoidandsnowflakemoved out ofcore.base. They read a wall clock and draw entropy, so they are not primitives of the language the wayMaybeandResultare. Update the mount path:mount core.id.uuid.{Uuid, v4, v7}; // was: core.base.uuidmount core.id.ulid.{Ulid, parse}; // was: core.base.ulidmount core.id.nanoid.{generate}; // was: core.base.nanoidmount core.id.snowflake.{Snowflake}; // was: core.base.snowflakeThe old paths never worked after the move —
core.basekept forward-declaring four modules whose sources had left, so every compile printed a "forward-declared but no source file exists" warning and anymount core.base.uuid;failed witherror<E402>: module not found. Both are gone.
Fixed — a renamed re-export is mountable from the module that renames it (2026-08-09)
-
public mount .sub.{X as Y};now really publishesY. A module can re-export a type under a name of its own choosing, and callers mount that name from the re-exporting module without knowing where the type is declared:// core/term/layout/mod.vrpublic mount .constraint.{LayoutConstraint as Constraint};// your codemount core.term.layout.{Constraint}; // was: error<E401>: cannot find// `Constraint` in module// `core.term.layout`The alias existed only in the module's re-export table and never reached the export surface the mount checker consults, so the compiler rejected a name its own module genuinely published. Renamed re-exports of FUNCTIONS were unaffected and keep working as before.
Fixed — glob mounts now deliver free functions (2026-08-08)
-
mount some.module.*;brings the module's FUNCTIONS into scope, not just its types. Until this fix a glob delivered types, protocols and constants, while every free function stayed unbound:mount core.prelude.*;fn main() {let r = range(0, 5); // was: error<E100>: unbound variable: range}A named mount of the same function always worked (
mount core.base.iterator.range;), so the two spellings disagreed about what a module exports. -
Scope.
mount core.prelude.*appears in over four thousand files, and the prelude re-exports most of its surface through globs — so the standard library's declared contract was only half in force.Maybe,ResultandListarrived;range,once,empty,unfold,min,max,sqrtdid not.printis a compiler built-in rather than a library function, which is why the prelude nonetheless looked alive. -
Why it was invisible. Nothing failed loudly — the standard library baked green and every build exited 0. The symptom was a large number of red tests, which reads as unfinished tests rather than as an unmet language contract.
-
Root. Archive entries are directory modules: everything in
core/base/*.vris stored undercore.base. Types carried the declaring file submodule alongside that entry path; functions and protocols did not, socore.base.iteratorpublished its types and none of its functions. A glob enumerates a module's export surface, whereas a named mount looks a name up and can fall back to the archive metadata — a fallback an enumeration cannot use, because asking by name means already knowing the name. -
Measured. In the conformance run,
text/textwent from 278 failing tests to none;base/memoryfrom 92 to 69;base/iteratorfrom 251 to 241.
Fixed — --target cross builds emitted host-shaped runtime code (2026-07-28)
- Cross-compiling for a different OS than the host now actually
targets that OS.
verum build --target x86_64-unknown-linux-gnurun from a macOS or Windows host — or any--targetnaming an OS other than the host's — correctly selected the target machine, frameworks, and compiler-rt, but the LLVM module's own triple, which is what every per-platform decision in the code generator actually reads, was never set from--target. Every cross build therefore emitted host-shaped runtime bodies inside a correctly-labelled object file. Concretely, this was a live no-libc violation: a Linux cross build emitted Darwin/libSystem-styledeclare @socketinstead of a raw syscall, leaving 10 undefined libc symbols in the object (bind,connect,listen,recv,recvfrom,send,sendto,setsockopt,socket,waitpid) — now 0. With no--targetflag, nothing changes (output is byte-identical). Not yet fixed: the separate no-libc linking configuration is not unconditionally coupled to--targetin every build path, so a cross build can still pick up host linker flags in some configurations — tracked separately, see no-libc architecture.
Fixed — tan and sincos returned wrong values for nonzero input (2026-07-28)
math.elementary's angle reduction used the wrong unit.reduce_trig_argreduced its input modulo π/4 whilesin,cos, andsincos's quadrant tables assumed a π/2 reduction — every angle came back in the wrong quadrant exceptx = 0, where the two conventions happen to agree.tan(1.0)returned-4.588…instead of1.5574…;tan(0.0)was correct before and after, which is why it read as a working control. Reducing modulo π/2 fixestanandsincos, the two callers whose return type let them run at all.sin,cos,exp,log, andsqrtincore.math.elementaryare separately broken right now — an unrelated stdlib-build defect currently causes calls to these five names to resolve to a same-named function in a different module instead of their own (surfacing as a runtime type error, or in at least one observed case a silently wrong value);exp2,cosh,tanh, andsinhinherit the failure because they callexpinternally. Not fixed yet — see the callout on the math stdlib page.
Fixed — type-safety & codegen (2026-07-24)
-
Refinement types are enforced on
letbindings and destructuring.let n: Int{it >= 0, it <= 10} = f();now traps at runtime —refinement violation: binding`n`— when the value is out of range. Previously the refinement was decorative on these binding forms while being enforced on function parameters; now tuple/array destructuring also checks each element against its own refinement (let (a, b): (Int{<= 5}, Int{<= 10}) = …traps on whichever element is out of range). -
Packed typed arrays work under AOT (Tier 1). Index read, write, and compound-assign on
[Int; N]/[Float; N]— e.g.xs[2] = xs[1] + 8— now compute correctly in native builds. Previously the typed read crashed because the native code generator had no load path for packed arrays (only the interpreter did). -
Float formatting rounds instead of truncating.
f"{3.9:.0}"now prints4andf"{3.14159:.4}"prints3.1416(previously3/3.1415). Rounding is half-up and carries correctly (f"{9.96:.1}"→10.0); the precision count was already right, only the last-digit rounding was off. -
A getter that returns a reference now formats as its value. A method returning an interior reference —
fn get(&self) -> &Int {&self.val }— now renders the referenced value in an f-string:f"{obj.get()}"prints42, not<object type_id=42>(nor the raw byte-list an earlier build showed). Only method-returned interior&self.fieldreferences were affected; a direct&localreference and an explicit deref (f"{*obj.get()}") always formatted correctly. The formatter now resolves the interior reference the same way method dispatch and*-deref already did.
A batch of fixes to the everyday experience of running Verum programs:
- String → integer parsing works.
Int.from_str("42")/Int.from_str_radix("2a", 16)(and everything layered on them —Int8…Int128parsing) were broken at runtime: the primitive treatedchar_at'sMaybe<Char>as aCharand panicked. Now returns the parsed value (orNoneon bad input) as intended. - No more spurious startup warning. Every program — even
fn main() { print("hi") }— printed a scary[run_global_ctors] WARN: … crashed via lenient-stub panic …for a benign, self-healing condition. It is now silent by default (setVERUM_TRACE_CTOR_SKIPto see it when debugging). verum runreports crash signals honestly. A child that dies on a signal (e.g.SIGSEGV) now exits128 + signumwith a named message instead of a misleading plainexit 1.- A program can define a top-level
module X { … }. Previously any inline module made the compiler losemain("No main function found"). f"{x:.N}"float precision is honoured.f"{3.14159:.2}"now prints3.14(the precision spec was previously ignored). This also fixed the underlying cause — an unannotated[0_u8; N]byte buffer now packs correctly instead of null-dereferencing when re-sliced across a call, which had latently broken every such scratch buffer.- Unsigned 64-bit integers display and compare correctly. A
UInt64with the high bit set (anything above2^63 − 1, e.g.UInt64.MAX) used to print as a negative number — value-to-text fell through to the signed path, sou64::MAXshowed as-1. It now renders as the true unsigned value at both the interpreter and AOT tiers. DirectUInt64comparisons (<,<=,>,>=,==,!=) likewise use unsigned semantics, so large values order correctly instead of wrapping to negative.
Grab a dev build to try them.
Fixed — arithmetic intrinsics AOT conformance: 129/129 both tiers (2026-06-29)
core-tests/intrinsics/arithmetic now passes 129/129 under both the
VBC interpreter and AOT (verum test --aot), up from interp-only.
Four fundamental AOT-codegen fixes:
-
Phi-predecessor blocks in
build_maybe_int_wrap— the AOT lowering ofchecked_add/sub/mul/divadded the mergephiusing block labels captured beforelower_make_variant, whosemalloc+ null-check SPLITS the block (the real edge into the merge comes fromvariant_ok). The stale labels made LLVM phi-elimination — even at-O0— write the value into a dead predecessor, so the function returned the inner payload instead of theMaybepointer andmatch checked_add(2,3) { Some(v) … }dereferenced the value-as-pointer → crash. Fixed by capturing the true predecessor viaget_insert_block()after the variant allocation. The same defect was eradicated inchecked_neg/absandList.first/List.last. -
checked_neg/checked_absreturnMaybe— they had returned the raw wrapping result and panicked oni64::MIN, instead ofNone/Some(result). -
wrapping_*_i8narrow return uses the i64 register model — a signed narrow (Int8) return was truncated toi8then zero-extended by the caller, losing the sign (-128read back as128). Narrow integer return types now lower toi64. -
Width-aware overflow arithmetic —
saturating/wrapping/overflowing/div-rem-absnow honour the per-op width + signedness operands instead of always computing ati64.
Refactored — kernel drift-defect collapse pass (2026-05-09)
Four cohesive single-source-of-truth refactors landed across the kernel crate. Each collapses a parallel hand-maintained data table into a single match expression with compile-time-checked exhaustiveness, turning silent runtime drift into structurally impossible mistakes.
-
Three normaliser variants →
normalize_core—normalize_with_budget,normalize_with_axioms_budget, andnormalize_with_inductives_budgethad drifted apart in ways that were genuinely unsound: the axiom-aware and inductive-aware normalisers silently dropped the cubical Kan-fibrancy reductions (HComp face-bot/top, Transp i1/Refl/const, Glue face-bot/top) that plainnormalizefired. Concrete consequence:definitional_eq_with_axioms(the SMT-routed equality check) could fail to identify provably-equal cubical terms once they were guarded by an axiom registry. Replaced with a singlenormalize_core(term, &mut NormaliseCtx)driver insupport.rs; ~750 lines of parallel structural recursion collapsed to one. New publicnormalize_fullis the canonical "all features on" entry point. -
CanonicalCert::expected_outcome+ helpers —crates/verum_kernel/src/canonical_battery.rshad its 24-cert battery's expected verdicts encoded twice: in the cert constructor's shape and in a parallelexpected_verdict(id)arm-table lookup. Folded the verdict intoCanonicalCertitself + added readability constructorsCanonicalCert::accept(...)/::reject(...). The freeexpected_verdict(id)is now a one-line shim over the battery walk; the previously-requiredevery_id_has_an_expected_verdictsanity test became vacuous (you can't construct a cert without a verdict). -
KernelRuleId::strict_intrinsic_suffix()— a hardcoded 7-armKernelRuleId → strict-intrinsic-namemapping inkernel_registry.rs::strict_tag_ofwas moved onto the enum itself (alongsidename()andfull_list()) so adding a rule is a single-place change.kernel_registry.rsnow callsrule.strict_intrinsic_suffix(); the free function is gone. -
KernelRule::meta()consolidation — three parallel methods on the 38-variantKernelRuleenum (name()/v_stage()/category()) collapsed into a single 38-armmeta()returningKernelRuleMeta. The legacy accessors become#[inline]projections. NewKernelRule::full_list()mirrorsKernelRuleId::full_list. Three new pin tests:meta_accessors_agree_for_every_rule,full_list_covers_every_variant,full_list_count_matches_soundness_corpus. DeprecatedKernelRule::citation()shim (zero callers) deleted. -
AntiPatternCode::meta()+AntiPatternBand— the largest reduction.AntiPatternCode(40 variants) had seven parallel data tables (code/name/docs_url/season/is_cve_ah/is_mtac/full_list). The per-pattern ordinal was encoded twice (literal"ATS-V-AP-NNN"incode()- integer in
docs_url()'s ordinal-mapping match) and could silently drift. IntroducedAntiPatternBand { Core, Base, Mtac, CveAh }carrying the canonicalband.season()mapping;AntiPatternCodeMeta { code, name, ordinal, band }with one 40-armmeta()source-of-truth. Eight drift pins added: code-format-matches-ordinal, name-matches-variant-id, ordinals-partition-1-to-40-uniquely, band-ordinal-ranges, is_*-agree-with-meta-band, season-cohort-partition, full_list-uniqueness, docs_url-format.
- integer in
Public API across all four refactors is additive only.
Existing accessors keep their signatures; new public types
(NormaliseCtx, KernelRuleMeta, AntiPatternBand,
AntiPatternCodeMeta) plus new methods (KernelRule::meta,
KernelRule::full_list, KernelRuleId::strict_intrinsic_suffix,
AntiPatternCode::meta) provide the consolidated source of
truth; legacy callers are unaffected.
The four refactors collectively added pin tests for every
collapsed parallel-data structure (meta-accessor agreement,
full_list uniqueness, ordinal partition checks). The kernel's
existing cargo test -p verum_kernel --lib suite covers the
new invariants alongside the structural rule set. Zero
behavioural regressions.
Added — Argon2id (RFC 9106) with OWASP-tuned profiles (2026-05-08)
core.security.kdf.argon2 — pure-Verum API around the memory-hard
Argon2id KDF with the three OWASP-2024-cheatsheet-canonical profiles
plus a Custom operator-supplied variant. Argon2idHasher impl of
the existing PasswordHasher protocol; full PHC-string round-trip
($argon2id$v=19$m=...,t=...,p=...$<base64-salt>$<base64-tag>);
constant-time tag compare.
- Profiles —
Sensitive(m=19 MiB, t=2, p=1; ~500 ms wall-clock; the OWASP-recommended default),Interactive(m=64 MiB, t=3, p=4; libsodium classic),Paranoid(m=256 MiB, t=8, p=1; KEK derivation),Custom { memory_kib, iterations, parallelism }. - Floor —
validate_paramsenforces OWASP 2024 floor on every hash AND every verify (m ≥ 12 MiB ∧ t ≥ 2; m ≥ 8·p; salt 16-256; tag 16-64) so a hostile PHC string can't push a verifier into DoS-amplified rounds. - Performance — memory-hard kernel routed through
@intrinsic("verum.argon2id.derive")so the runtime backs it with a constant-time native implementation.
Re-exported behind core.security.password_hash so existing
PasswordHasher-using callers swap with one line.
Added — postgres Row ORM façade + parameterised query API (2026-05-08)
core.database.postgres.row — column-name-keyed view over
(RowDescription, List<WireValue>) pairs returned by
PgConnection.simple_query / execute_prepared_typed. Eight typed
getters (get_text / _opt, get_bool, get_int / _opt,
get_bytes, get_timestamp / _opt, get_text_array) cover the
registered postgres scalar / array families with per-OID guards,
NULL discipline (_opt for nullable columns), and TEXT + BINARY
wire-format coverage on every path.
AsyncPgPoolGuard gains four ergonomic parameterised query methods
that route through prepare → execute_prepared_typed → close_prepared
internally and project rows into the Row façade:
let rows: List<Row> = conn.query(sql, params).await?;
let row: Row = conn.query_one(sql, params).await?; // SQLSTATE 02000 on zero
let opt: Maybe<Row> = conn.query_one_opt(sql, params).await?; // Ok(None) on zero
let tag: Text = conn.execute_with_params(sql, params).await?;
Improved — inline-prelude propagation + multi-file mount-import recursion (2026-05-08)
Three-layer fix targeting "type not found" failures when downstream
projects mount stdlib types via the canonical short-form path
(mount database.postgres.row.Row, mount collections.List,
mount encoding.json.{...}, etc.):
verum_types::collect_inline_mount_reexports_recursive— recursive walk over inline-module items + every nested public submodule's Mount re-exports. Surfaces every name the canonical prelude pattern (public module prelude { public mount super.collections.List; … }) places under a top-level mount glob.verum_types::is_stdlib_toplevel_path— centralised prefix discriminator for every immediatecore/subdirectory (48 entries).verum_modules::propagate_submodule_reexports— surface explicit Path/Nested re-exports inside a public submodule into the outerExportTable.verum_compiler::pipeline::compile_orchestrationPass 0 — recursiveprocess_imports_recursivedescends into nestedItemKind::Modulebodies (anchoringsuper/selfresolution at the outercurrent_module_path).
Fully general — no hardcoded "prelude" name; the recursion fires for any inline submodule that re-exports public names.
Fixed — static-method dispatch for stdlib types (2026-05-06)
Type.static_method(...) calls against stdlib types
(Text.with_capacity(64), Text.new(), Text.from_str("..."),
…) now type-check and execute end-to-end. Pre-fix every such
call failed type-check with no method named X found for type Y,
or — for the small surface that did pass type-check via the
AST-driven path — returned Unit at runtime and crashed the
downstream method chain with method not found on receiver of runtime kind ().
The closure spans both layers:
- Type-check — the metadata-driven inherent-method
registration now registers static methods (no
selfreceiver) under both the instance-dispatch key and the static-dispatch key. Closes ~134 of 158 Text statics plus every static surface onList,Map,Heap,Shared,Path,TextBuilder,WireBuf,Deque,BinaryHeap, etc. The archive name-harvester also walks function bodies- impl items + const / static initialisers (previously
mount-only), so a file withmount core.*glob no longer bypasses the archive load.
- impl items + const / static initialisers (previously
- Runtime — the canonical
Text.*static factories (Text.new,Text.with_capacity,Text.try_with_capacity,Text.from_static,Text.from_str,Text.from_utf8,Text.from_utf8_lossy,Text.from_utf8_unchecked,Text.from_char) now flow through the native intercept layer and return their canonical values without depending on archive-side bytecode bodies.
Verification:
Text.with_capacity(64).len()→0(was: runtime crash).Text.new()returns an emptyText(was: returnedUnit)."abc".to_uppercase()→"ABC"(was: type-check error).
Out of scope for this batch: List, Map, Heap, Shared
static factories that aren't intercepted still type-check but
return Unit at runtime until either their native intercepts
land or the deeper archive-injection fix closes the generic
case.
Documented at Standard library → Text and Architecture → Runtime tiers § Native intercept layer.
Added — ATS-V transitive multi-hop closure for AP-019 + AP-024 (2026-05-06)
The cross-cog peer-graph dimension of architectural typing now
walks the full composes_with graph, not just direct one-hop
peers. Indirect chains where lifecycle or foundation drifts at
depth ≥ 2 (e.g. theorem T → axiom A → hypothesis H where every
adjacent edge is locally legal but the transitive composition
silently inherits hypothesis-strength) no longer slip through.
Previously Session.arch_shape_registry already snapshotted
every cog's parsed shape, but the resolvers only inspected
direct edges. This release introduces the kernel-side DFS
walker that policy adapters can compose against.
- Walker —
verum_kernel::arch_transitive(new module).for_each_transitive_peer<F>(start, registry, &mut F)does a depth-first walk with built-in cycle prevention via a visited-set, bounded byMAX_TRANSITIVE_DEPTH = 32. Visits surface asPeerVisit { path, shape, depth, intermediate(), terminal() }with aControlFlow<()>return so callers can short-circuit. - Policy adapters —
resolve_transitive_lifecycle_regressions(closes AP-024) andresolve_transitive_foundation_downgrades(closes AP-019) filter visits bydepth ≥ 2, leaving the direct one-hop dimension untouched. - Pipeline wiring —
PhaseInputsgained two new fields (transitive_lifecycle_regressions,foundation_downgrades) that propagate intoDiagnosticContextthroughrun_arch_phase_one_with. Two newSessionhelpers (resolve_transitive_lifecycle_regressions,resolve_foundation_downgrades) snapshot the registry and dispatch into the kernel resolvers.run_arch_phase_for_attrs_registry_awarepopulates both fields from the parsed shape. - Pin tests — four new entries in
crates/verum_kernel/tests/k_arch_v_alignment.rsraise the cross-side pin total from 39 → 43:pin_transitive_walker_present,pin_phase_inputs_transitive_fields_present,pin_session_transitive_resolvers_present,pin_transitive_resolver_correctness.
The walker is reusable: future cross-cog anti-patterns (AP-018
CompositionPathDeception, AP-022 CapabilityLaundering, …)
that need depth-aware traversal compose against
for_each_transitive_peer instead of re-implementing DFS.
Documented at Architecture-as-Types → Audit protocol §10, ATS-V → Anti-patterns AP-019/AP-024, ATS-V → Cross-side pin tests §7.
Added — VBCA Layer E protocol-method type-param scope (2026-05-06)
Generic protocol methods that project associated types
(Self.Item-shaped paths) through variant payloads now flow
through a method-local generic-parameter scope, eliminating the
fallback path where Self.Item rendered as the Type::Concrete
PTR sentinel. Three coupled fixes:
- The combined
generic_param_mapat the protocol-variant emit site routesresolve_field_type_refwith proto-level- method-local + synthetic-Self IDs.
Selflives at0x4000+N; associated-type paths hash into0xC000..0xFFFF.
- method-local + synthetic-Self IDs.
resolve_field_type_refextended to handleTypeKind::Function,TypeKind::AssociatedType, andTypeKind::Qualified— the fast-parser producesQualifiedforSelf.Item, notAssociatedType.PathSegment::SelfValuerecognition was load-bearing — the naïvePathSegment::Namefilter returned the empty string, breaking synthetic-Self lookup. Fixed.
After Layer E, parse_descriptor_type_string cleanly converts
__generic_N placeholders to fresh Type::Var instances; the
prior coarse safety guard (registering wrong-arity-name fallbacks
at register_standard_protocols time) was removed.
L1/generics 18/20. bounds.vr and higher_kinded.vr both pass.
Unit tests 1011/1011.
Added — VBC bytecode encode/decode symmetry (2026-05-06)
Three classes of asymmetry between encoder and decoder broke sequential bytecode decoding (linker, disassembler, validator):
Rank2Functiondecode arm — encoder wrote discriminant0x08for rank-2 polymorphic function types (Transducer-shaped); the decoder hitInvalidTypeRef.- Extended-opcode length prefix — 9 extended opcode
families (
Arith/Ffi/Math/Simd/Char/Cbgr/Text/Mem/Log) encodedopcode + sub_op + raw_operand_byteswith no length information; the decoder returned empty operands and corrupted the stream pointer. Fix: encoder now writesvarint(len) + bytesaftersub_op; decoder reads the length and consumes bytes via the newdecode_extended_operandshelper. 9 interpreterhandle_*_extendedpaths skip the varint afterread_u8(sub_op). - Missing decoder arms —
TlsGet,TlsSet,GetVariantData,GetVariantDataRefwere encoded but had no decoder arms; sequential decoders fell through to the wildcardRawand corrupted*offset. Added.
TensorExtended/GpuExtended/the Extended carrier remain on
their structural per-sub_op decoders (they were already
length-symmetric).
Tests: verum_types 1011/1011, verum_vbc 1100/1100,
build-paper.vr typecheck clean. linker_round_trip_through_embedded_archive
remains failing on a separate ~95-decoder gap (Concat,
MakeVariant, Push/Pop, …) — same architectural class, tracked
incrementally.
Added — core.text.numeric.decimal foundational arithmetic (2026-05-04)
A new foundational stdlib module landed:
core.text.numeric.decimal. Pure-stdlib implementation, zero
new AST nodes or compiler intrinsics.
Decimal { coefficient: Int, scale: Int }with full parse / render /add/sub/mul/div/compare.RoundingModeenum:HalfEven(IEEE 754 banker's rounding, the default eliminating +0.5 bias),HalfUp,HalfDown,Truncate.DivByZeroerror variant;div(&self, &other, precision, mode)plus conveniencediv_round(HalfEven default) anddiv_trunc(precision = 0, Truncate).- Scale-aligning arithmetic via a 19-entry
POW10const table; full overflow detection (checked_add/checked_sub/checked_mulincludingi64::MINsentinel handling). - Sign-aware long division on
i64coefficients with a sticky-bit tracker for round-half tie semantics:(-7) / 2HalfEven yields-3.5.
V0 boundary: i64-cap on coefficient (~18 significant digits),
MAX_SCALE = 18, no BigInt, no scientific-notation parsing.
Division, banker's rounding, and the full V0 surface now ship.
This unblocks PG NUMERIC text → binary encode (V1, also
landed; see below), money types, and anywhere financial
precision was previously gated on a lossless decimal type.
Documented at Standard library → Decimal.
Added — Postgres wire codec — NUMERIC, tsvector, composite (2026-05-04)
The remaining three deferred types in core.db.postgres.codec
all received V0 binary codecs, closing the deferred list:
- NUMERIC (
PgNumeric) — full-fidelity decode plus integer encode (encode_numeric_from_int). V1 text → binary encode (encode_numeric_from_decimal) bridges through the new Decimal library, so non-integer parameter binding no longer falls back toFmtText. Five sign-code constants (PG_NUMERIC_{POS,NEG,NAN,PINF,NINF}). - tsvector (
PgTsVector,PgTsLexeme,PgTsPosition) — decode-only V0 (header reader + per-lexeme NUL-scanner + position-array decoder). Encode is V1 — most callers shipTextand let PG runto_tsvectorserver-side. ConstantsPG_TS_WEIGHT_{D,C,B,A}. - composite / record (
PgComposite,PgCompositeField) — full bidirectional codec. Decoded fields surface as(type_oid, ArenaSlice)pairs; callers dispatch via the existing per-OID decoder table sql# uses for top-level columns. Auto-encode from a Verum record value (derivingtype_oidplus per-field encoder) is V1 — depends on the row-resolver.
All three types are wired into supports_binary. Every PG
built-in scalar plus composite plus array now has a wire codec.
Test fixtures at
vcs/specs/L2-standard/db/postgres_numeric_codec.vr exercise
all three plus the Decimal-backed NUMERIC encode path.
Added — AOT no-libc f64 / strtol formatting trio complete (2026-05-04)
P-AOT-NO-LIBC-F64-FMT-V0 closes the third and final piece of
T-DEFER-AOT-NO-LIBC. The AOT-emitted formatting / parsing
surface is now fully libc-free across all three pieces:
- i64 → decimal —
verum_internal_i64_to_decimal(already shipped at 3a111f8e). - strtod — pure-IR strtod (3bbd867c).
- f64 → decimal —
verum_internal_f64_to_decimalLLVM IR helper (this release). Bit-pattern NaN / Inf / zero special-cases,fptosiinteger part composed with the i64 helper, frac handling viafptosi(frac × 1e6)plus 6-digit zero-pad div / rem loop with trailing-zero strip.emit_verum_float_to_textswapssnprintf("%g")for the new helper through the runtime bridgeget_or_declare_internal_f64_to_decimal.
V0 boundary: no scientific notation, |v| > i64::MAX saturates,
no round-half-to-even.
Companion sweep at 45cd3904 — runtime.rs::get_or_declare_strtol
was forward-declaring verum_internal_strtol bodyless and
relying on a separate emit path to fill it. Programs hitting
verum_text_parse_int without also tripping the VBC
instruction-lowering path linked against an empty
mov w0, #0; ret stub — silent zero from every parse_int call.
Fixed by routing through instruction.rs::get_or_declare_internal_strtol
with the adopt-and-emit pattern (mirror of strlen). Dropped
get_or_declare_snprintf and get_or_declare_strtod — both
were libc-symbol emitters with zero remaining callers.
Documented at Architecture → No-libc invariant.
Added — count_o_unbounded recognizer wires V2 dispatch into verify_refinement (2026-05-03)
The V2 count_o_dispatch deliverable becomes load-bearing: a
new pure AST-walking recognizer
(verum_smt::count_o_recognizer) sits as a pre-pass at the top
of RefinementVerifier::verify_refinement, detects the
canonical conjunctive shape, materialises a CountOQuery, and
routes the verdict through the dispatcher.
- 4-variant rejection classification (
NotCountOPredicate/NoBoundClause/UnsupportedClosure/UnsupportedPredicateBody) for telemetry. - Pattern matrix covers
Le/Lt/Ge/Gt/Eqbound clauses, both arg orders, both unqualified and fully-qualifiedcount_o_unboundedpaths, and nested conjunctions. - Lt/Gt bounds normalise to LessEq(K-1) / GreaterEq(K+1).
@verify(runtime)skips the pre-pass.- 12 new recognizer tests + integration into the
RefinementVerifier::verify_refinementpre-pass.
Distinct from count_o_dispatch itself (which shipped earlier
today): this is the wiring that makes the dispatcher actually
trigger from user-source refinement predicates.
Added — the SMT backend Finite Model Finding dispatch for count_o_unbounded (2026-05-03)
OWL 2 count_o_unbounded(Maybe.None, pred) calls inside a
refinement type carrying an explicit cardinality bound now
dispatch to the SMT backend Finite Model Finding instead of always
returning Maybe.None.
- New
verum_smt::count_o_dispatchmodule with a focused pipeline:CountOQuery→ SMT-LIB assertion →FmfQueryover an uninterpreted individual sort with cardinality ≤ K → the SMT backend model enumeration → count extraction from thepred_odefinition (handles disjunctive bodies,true/falseshorthand, and clamps at the discovered domain size). - Four-variant
CountOResultorthogonal to the SMT backend's sat/unsat verdict:Decided(load-bearing),BoundExceeded(promote V1 warning to a hard error),Unsupported(the SMT backend not linked → V1 fallback),Timeout. flag_count_o_dispatchintegrates with the capability router by settingneeds_finite_model_finding=trueon the goal'sExtendedCharacteristics; FMF policy already routes to the SMT backend.- Reuses every existing SMT piece —
smt_backend_advanced::FmfQuery/find_finite_model/SmtAdvancedError::NotAvailable— no new FFI surface.
Documented at Verification → OWL 2 §5 and Verification → SMT routing.
Added — Three-kernel differential architecture (2026-05-03)
The differential-kernel gate now aggregates three independent algorithmic implementations on every certificate:
- Algorithm A —
proof_checker.rs: bidirectional type-check with explicit substitution + WHNF (the trusted base). - Algorithm B —
proof_checker_nbe.rs: closure-based NbE with level-indexed quote. - Algorithm C (new) —
KernelV0Kernel: manifest-driven bootstrap verifier anchoring structural type-check, every kernel_v0_manifest rule's audit-clean discharge status, the meta-soundness footprint, and per-rule strict-intrinsic dispatch.
Algorithm C is structurally orthogonal to A and B — it consults
the bootstrap manifest + meta-soundness registry rather than
re-walking the certificate's term — so a manifest-discharge
drift surfaces as a slot-C-only failure even when A and B's
runtime rules remain unchanged. KernelRegistry::default() now
registers all three; any pairwise disagreement fails the audit.
Documented at Verification → Three-kernel architecture.
Fixed — core/database/sqlite/alter clone preserves validation-error variants (2026-04-29)
The ALTER engine's clone_failure collapsed every
AefValidation(e) into AefExecution(error_kind(e)) with a
"we can't deeply clone here" placeholder. Two pieces of
information were lost on every phase clone:
- Variant identity: validation errors became execution errors
downstream; code matching on
EpFailed(AefValidation(_))never matched after a clone. - Structured payload:
error_kindreduces a 6-variant sum carrying table/column names to a single discriminant tag string ("no_such_table", "primary_key", …). TheDisplay for AlterValidationErrorimpl that produces user-facing messages like"sqlite ALTER table.col: cannot alter PRIMARY KEY column"was unreachable for any cloned phase.
Replaced the placeholder with a proper variant-aware deep clone
(clone_validation_error) that mirrors the existing
clone_delta pattern in the same file: explicit pattern match
on every variant, each Text payload cloned. Adding a new
AlterValidationError variant now surfaces here as a
non-exhaustive-match build error rather than silently losing
data.
Fixed — SMT exhaustiveness witness extraction matches scrutinee sort (2026-04-29)
Fourth closure in the SMT exhaustiveness chain. Pre-fix
get_model hardcoded Type::Int for both the formula
translation and the var-extraction sort. Bool guards (e.g.
if x where x is Bool) had their bindings declared as
Int::new_const("x") — a SEPARATE the SMT backend var in the WRONG sort
from the model's actual Bool sort. model.eval then returned an
arbitrary fresh-Int evaluation rather than the solver's actual
Bool decision, surfacing as garbage witness data in user-facing
exhaustiveness diagnostics.
The fix dispatches on scrutinee_ty:
Bool→Bool::new_const+value.as_bool()→SmtValue::Bool- Default → existing Int path (preserved for the most-common arithmetic-guard case)
extract_uncovered_witnesses forwards scrutinee_ty (was
_scrutinee_ty, parameter name documented its non-use). Float /
Char / Text are deferred — formula_to_smt itself can't lower
them today, so adding extraction arms would silently never fire.
Combined with the prior three fixes
(467ed00d + 53f0be85 + 2a069f04), SMT-backed exhaustiveness
now verifies AND extracts witnesses end-to-end without any
placeholder / hardcoded data, across both supported sorts.
Fixed — SMT exhaustiveness witness extraction walks the formula (2026-04-29)
Third closure in the SMT exhaustiveness chain, completing the
end-to-end fix that started with the guard-Expr lift. The
witness-extraction helper get_model (called by
extract_uncovered_witnesses to surface uncovered cases as
diagnostic hints) only ever asked the the SMT backend model for a binding named
literally "n". Any guard mentioning a different identifier
(x, value, count, …) produced a witness with an empty
bindings map, which user-facing diagnostics rendered as _. A
guard combining n with a second variable returned a phantom
witness — n wasn't even in the formula, but the helper asked
The SMT backend about it anyway and surfaced an arbitrary value as if it were
load-bearing.
The fix walks the formula structurally collecting every
Var(name) reference (across Bool/Int leaves, Binary
l+r, Not/Neg inner, Or formulas), deduplicates and sorts
the result for snapshot-stable diagnostics, and asks the SMT backend for each
binding's value. collect_var_names is the new private helper;
its contract pins three load-bearing properties: all branches
walked, pure-literal formulas yield empty, duplicates preserved
(dedup is the caller's contract).
Combined with the prior two-step fix (467ed00d + 53f0be85),
SMT-backed exhaustiveness now verifies guards end-to-end without
any placeholder/hardcoded data. A guard like n if n > 0 now:
(1) lifts its Expr to PatternRow.guard, (2) lifts "n" to
PatternRow.bindings so the SMT translator can resolve it,
(3) yields a witness with the actual n = -1 value when
verification reports the case as uncovered.
Fixed — Pattern bindings reach SMT-backed exhaustiveness verification (2026-04-29)
Companion to the prior guard-expression lift. Without this, the
SMT exhaustiveness path was lifting the guard expression
correctly but then dropping it: with bound_vars empty, the
translator's bound_vars.contains_key(name) check returned false
for every variable reference (e.g. n in n if n > 0), which
made the entire guard expression translate to None →
unknown_guards → fall back to the conservative non-SMT verdict.
The earlier fix had moved the SMT path from "always-exhaustive
false-positive" to "always-skipped" — sound but doing no useful
work in any guard that mentions a pattern-bound variable (which
is most real-world cases).
The fix lifts pattern bindings into the matrix:
PatternRow.bindings: List<Text>carries the row's pattern- bound identifier names. Populated only for guarded rows (the field's only consumer is the SMT path; populating for plain patterns would be wasted work on every match site).collect_pattern_bindings(&Pattern)walks the AST recursively over identifier, tuple, array, slice, record (including punned{ name }fields), variant tuple/record, or-arms, and-conjuncts, reference inner, TypeTest binding, and Active bindings.- All 9 specialization sites propagate
bindingssymmetrically withguard. TheTypeTestexact-match arm strips both fields together since the specialized row no longer carries the conditional. extract_guarded_patternspopulatesbound_varsfromrow.bindings, attaching the scrutinee type as a sound placeholder. Per-binding types from type-inference are a future precision pass; the SMT translator's current contract only consults the keys.
Two pin tests cover the binding-extraction contract:
n if n > 0 lifts ["n"], and (a, b) if a < b lifts both
"a" AND "b" (regression that walks only the first sub-pattern
surfaces here as the SMT verifier silently dropping any guard
that mentions the second binding).
Fixed — SMT-backed exhaustiveness no longer false-positives on guarded matches (2026-04-29)
The pattern coverage matrix carried only a has_guard: bool flag
per row; the actual guard expression from
PatternKind::Guard { pattern, guard } was thrown away during
matrix construction. The SMT-backed exhaustiveness verifier
(extract_guarded_patterns) substituted a placeholder true
literal for every guard, which trivially proved every all-guarded
match exhaustive. Enabling
ExhaustivenessConfig.use_smt_guards = true therefore produced
false-positive exhaustiveness verdicts on real guard sets that did
NOT cover all cases — silently defeating the entire SMT-backed
verification path.
The fix lifts real guard expressions into the matrix:
PatternRow.guard: Option<Arc<Expr>>carries the top-level guard expression (Somewhen the pattern's outermost kind isPatternKind::Guard).build_matrixpeels the guard before recursing; specialization propagates the field through all 10 row-construction sites.extract_guarded_patternsconsumes the real expression. Rows whose guard is nested inside an or-pattern (where the expression isn't liftable to row level) are skipped at the SMT boundary; the conservative non-SMT all-guarded verdict still applies to them.
Pin tests cover both directions of the contract: (1) a top-level
Guard pattern lifts its expression structurally into row.guard
(regression re-introduces the false-positive SMT path), (2) a
non-guarded pattern leaves row.guard = None (regression would
route every match through SMT, exploding compile time).
This closes the explicit implementation TODO that was sitting in
the extract_guarded_patterns body ("A full implementation would
parse the guard from the AST").
Fixed — VerumFormatConfig.trailing_commas reaches format_field_list (2026-04-29)
The LSP formatter's struct-field emission always wrote ,\n after
every field including the last, regardless of the configured
stance. Setting trailing_commas = false in the editor's format
config had zero observable effect on formatted output — every
formatted struct produced a trailing comma.
Wired at the field-emission site: collect the field list, track
the last-field index, and suppress the comma only on the final
field when the toggle is off. Intermediate fields keep ,\n so
the separator semantics are preserved. Default remains true
(the documented house style).
Other inert fields on the same struct (max_line_width,
align_assignments, preserve_blank_lines) require larger
refactors (line-wrapping engine, pre-scan alignment, source-
position blank-line tracking) and stay deferred to dedicated
commits — wiring them with placeholder logic would silently
change every formatted source file's output.
Fixed — [llvm].{target_triple, target_cpu, target_features} reach the AOT pipeline (2026-04-29)
The manifest's [llvm] section was parsed into a LlvmConfig
struct on the CLI side but never plumbed downstream. Declaring
target_triple = "x86_64-unknown-linux-gnu",
target_cpu = "znver3", or target_features = "+avx2,+fma" in
verum.toml had zero observable effect — the AOT pipeline always
called TargetMachine::get_host_cpu_name() /
get_host_cpu_features() for native builds, and --target was
the only working knob for cross-builds. Reproducible-CI workflows
that pin a fixed microarchitecture / target triple in the
manifest were silently producing host-tuned binaries.
Wire-up:
- New
CompilerOptions.target_cpu: Option<Text>andtarget_features: Option<Text>. DefaultNone; the AOT pipeline preserves the previous behaviour (host detection on native,"generic"/ empty on WASM) when both areNone. cli::commands::build: precedence is--targetCLI flag >[llvm].target_triple> host default;[llvm].target_cpuand[llvm].target_featurespopulate the new options when unset. CLI doesn't expose--target-cpu/--target-featurestoday; the manifest is the user-facing knob.pipeline.rs::phase_generate_native: the host-detection branch now goes viacpu_override.unwrap_or(host_default)/features_override.unwrap_or(host_default). WASM still short-circuits to"generic"/ empty when no override.
Two contract tests pin: (1) target_cpu and target_features
default to None (load-bearing for the WASM-cross fallback that
relies on None to mean "use the WASM defaults"), (2) the
field-shape round-trip so a refactor can't drop or rename the
fields without surfacing in CI.
Fixed — [verify].distributed_cache_trust reaches VerifyCommand end-to-end (2026-04-29)
Closes a chained inert-defense gap that spanned the manifest
parser, the CLI profile-config layer, the compiler-side options
struct, and the verification command itself. Previously,
[verify].distributed_cache_trust in verum.toml was parsed by
the CLI but never plumbed downstream — and even
distributed_cache_url (the URL it gates) only reached
CompilerOptions to die there: VerifyCommand::new always
constructed the cache via VerificationCache::new(), so
configuring a distributed verification cache in verum.toml had
zero observable effect on verum verify.
Wire-up (top-down):
[verify]and[verify.profiles.<name>]: newdistributed_cache_trustfield on both with the standard profile-merge precedence (with_profileoverrides parent).ProfileConfig::distributed_cache_trust: populated frommerge_with_manifest, forwarded toCompilerOptions.distributed_cache_trustalongside the URL.CompilerOptions.distributed_cache_trust: documents the three accepted policy strings ("all"/"signatures"/"signatures_and_expiry") and the unknown-value fallback.VerifyCommand::new: whendistributed_cache_urlis set, constructs aCacheConfig::default().with_distributed_cache(...)carrying the parsedTrustLeveland usesVerificationCache::with_config(cfg)instead of::new().
The parse_trust_level helper is the load-bearing safety
boundary: ASCII-lowercases + trims the input, accepts the three
documented values, and explicitly falls back to Signatures
(with a tracing::warn) on anything unknown — a typo in
verum.toml MUST NOT silently downgrade trust to All.
Documented unknown-value behaviour: warning emitted, safe baseline
applied. Three pin tests cover the default-when-missing,
documented-value, and unknown-fallback-to-signatures cases.
Fixed — CompilerOptions.emit_mode Assembly and Object variants short-circuit before linking (2026-04-29)
Companion to the prior wire-up that closed EmitMode::LlvmIr
and EmitMode::Bitcode. The terminal variants Assembly
(set by verum build --emit asm) and Object landed on
CompilerOptions but the pipeline always proceeded through
runtime-stub compilation and linking, silently producing an
executable instead of the requested artifact.
The variant set splits into two semantic classes:
- Additive (
LlvmIr,Bitcode) — emit the artifact alongside the executable; the build still produces a linked binary. Useful for inspection / diagnostics. - Terminal (
Assembly,Object) — emit the artifact instead of the executable; the build short-circuits before runtime stubs and linking. Useful for downstream custom-link toolchains and embedded targets.
Wired in phase_generate_native immediately after the bitcode
emit block: Assembly calls target_machine.write_to_file
with FileType::Assembly to target/<profile>/<module>.s
(or the user-specified --output); Object copies the
already-written object file from the build dir to
target/<profile>/<module>.o (or --output). Both modes
return early before any linker invocation.
Fixed — LspConfig.verification_show_cost_warnings + verification_slow_threshold reach the LSP response (2026-04-29)
Both fields were forwarded into the validator's internal
state and the validator owned a should_emit_cost_warning
gate plus a slow_threshold() accessor — but no path actually
consumed them. Setting verum.lsp.verificationSlowThreshold = 1000
in the client config had no observable effect on
validateRefinement responses.
Wired at result_to_response: when the elapsed wall-clock
crosses the configured threshold AND
verification_show_cost_warnings is on, prepend a synthetic
W0701-coded WARNING-severity diagnostic to the response's
diagnostics list. The message names the configured threshold
so users can identify which manifest knob to tune.
Fixed — CompilerOptions.enable_cbgr_elimination reaches LLVM lowering (2026-04-29)
The session-level toggle for AOT-side CBGR check elimination
(default true) was documented but never forwarded to
LoweringConfig.cbgr_elimination. The corresponding
LoweringConfig field DID gate codegen behaviour, but the
session-level option was held for show — flipping it had no
effect on emitted LLVM IR.
Wired via .with_cbgr_elimination(...) in the LoweringConfig
builder chain alongside with_opt_level / with_debug_info /
with_coverage / with_permission_policy.
Fixed — ValidationConfig.{fail_on_mismatch, log_mismatches} are load-bearing (2026-04-29)
Two ValidationConfig fields on the SMT backend switcher
were declared, defaulted, and never read. The existing
cross-validation divergence path hardcoded both "log to
stderr" and "return Error" regardless of caller policy.
Wire both at the divergence branch:
log_mismatches(defaulttrue): whenfalse, suppress the stderr warning. CI pipelines that capture and report divergences through the routing-stats sink avoid duplicate noise.fail_on_mismatch(defaultfalse): whenfalse, return the the SMT backend result rather than a hardError— the documented "log but don't fail" policy. The Certified strategy sets the flagtrueto surface divergence as a build failure.
Routing-stats divergence-event recording is unconditional (machine-readable telemetry is the canonical source); the human-readable stderr line and the hard-error return are now the gated user-facing surfaces.
Fixed — FallbackConfig.on_timeout distinguishes timeout from generic unknown (2026-04-29)
The flag was set in defaults and the manifest parser but no
code path consulted it. the SMT backend surfaces timeouts as
SolveResult::Unknown with a reason string, so the existing
on_unknown branch caught timeouts as a side-effect — but
the two cases are conceptually distinct.
Wire as a more-specific Unknown branch ahead of on_unknown:
when the reason string mentions timeout / canceled /
resource, consult on_timeout. Empty / non-matching
reasons fall through to the generic branch.
Callers can now opt to fall back on timeouts only (when the alternative solver might have a different complexity profile) without falling back on genuine unknowns (where both solvers are likely to give up).
Added — verum cog-registry distribution registry surface (2026-04-29)
Verum's package manager joins the small group of systems with cryptographic proof-integrity: every published cog ships with a per-cog reproducibility hash chain (sources + build env + output + canonical chain hash), typed attestations (verified_ci / honesty / coord / cross_format / framework_soundness), and immutable releases (republishing a version with different content is a hard failure).
Six subcommands:
verum cog-registry publish --manifest FILE [--root DIR] [--registry-id ID] [--output ...]— publish a cog manifest.verum cog-registry lookup --name N --version V [...]— fetch a specific (name, version) pair.verum cog-registry search [--name SUB] [--paper-doi DOI] [--framework TAG] [--theorem NAME] [--require-attestation KIND]— multi-criteria search.verum cog-registry verify --name N --version V [...]— envelope chain-hash integrity check + attestation inventory.verum cog-registry consensus --name N --version V --mirror DIR [--mirror DIR]…— multi-mirror cross-check. Non-zero exit on any disagreement.verum cog-registry seed-demo [--output ...]— populate a demo registry; useful for tutorials and the docs generator.
Three trust invariants enforced at publish time + lookup:
- Immutable releases —
(name, version)is a permanent binding to a chain hash. - Envelope integrity —
chain_hashMUST match the canonical derivationblake3(input_hash || build_env_hash || output_hash). Tampering observable. - Multi-mirror consensus — the trusted answer is the chain hash every mirror agrees on; one disagreement breaks consensus.
V0 ships production-grade MemoryRegistry and
LocalFilesystemRegistry (one JSON file per cog version under
<root>/<name>/<version>.json); V1+ adds an HTTP server fronting
the same trait surface (packages.verum.lang) with Ed25519
signature verification. CLI flags and JSON schemas are unchanged
across V0 → V1.
Full guide: Tooling → Cog distribution registry.
Added — Tier-0 interpreter networking + Weft framework documentation (2026-04-29)
The VBC interpreter now backs __tcp_*_raw and __udp_*_raw
intrinsics with real std::net sockets through a thread-local
synthetic-fd registry. Verum-script mode and verum run --interp
can now bind ports and serve traffic — previously the intrinsics
returned -1 for every call, which made interpreted-mode networking
a documentation-only feature.
mount core.intrinsics.runtime.os.{
__tcp_listen_raw, __tcp_accept_raw,
__tcp_recv_raw, __tcp_send_raw, __tcp_close_raw,
};
fn main() {
let listen_fd = __tcp_listen_raw(7878);
let conn_fd = __tcp_accept_raw(listen_fd);
let req = __tcp_recv_raw(conn_fd, 4096);
let _ = __tcp_send_raw(conn_fd, "HTTP/1.0 200 OK\r\n\r\nhello");
__tcp_close_raw(conn_fd);
__tcp_close_raw(listen_fd);
}
The full Weft stack (through core/net/tcp.vr plus the io-engine
async layer) currently still requires AOT — Tier-0 syscall
emulation (accept4, read, write through syscall_raw) is
on the roadmap.
The framework documentation has been substantially expanded — eleven
new and rewritten pages in stdlib/net/weft/ covering the full
spec surface: Service / Layer / ServiceBuilder, Handler & FromRequest
extractors, Router, Listener, Error model, Refinement-typed routes,
Connection pipeline, Transport protocol, Backpressure layers, SPIFFE
identity, Deterministic Simulation Testing.
QUIC and TLS index pages updated to reflect actual conformance numbers as of 2026-04-29 (60 % and 56.6 % respectively) and to document the four cross-cog symbol-resolution issues that block the remaining tests — none of the failures point at protocol-level defects.
Fixed — CodegenConfig.validate default-off until stdlib emit is clean (2026-04-29)
Commit 1c4ddcc1 wired CodegenConfig.validate to actually run
the post-emit structural validator instead of being a documented
no-op. The validator immediately surfaced approximately 8 400
pre-existing encoding bugs in stdlib bytecode, blocking
verum run --interp and verum run --aot for every non-trivial
program.
The default has been reverted to false until the encoding bug
class closes; CI gating that wants strict structural checks opts
in via with_validation() or config.validate = true. This matches
the existing strict_codegen pattern (lenient default, opt-in
strict). Pin tests updated to document the new contract.
Fixed — Three Weft VCS regressions; promoted Json<T> to spec name (2026-04-29)
vcs/specs/L2-standard/net/weft/error_shape.vr— referencedErrorCategory(resolved to a different cog's type with different variants) instead ofWeftErrorCategory. Test now uses the canonical name; the underlying API is unchanged.vcs/specs/L2-standard/net/weft/json_extractor.vr— used Rust-style turbofishextract_json::<T>(...); Verum grammar uses the spacelessf<T>(args)form. Stripped the 12 occurrences. The module's user-facing extractor type was promoted fromWeftExtractorJson<T>to the spec-canonicalJson<T>, dropping the placeholder inhandler.vrand theJson is JsonValuealias incore.encoding.json(which madeJsonValuethe single canonical value-tree name).vcs/specs/L2-standard/net/weft/dst_basic.vr— used Rust-style lifetime annotation&'static Textonpub const PROPERTY_*. Verum form isText. Test surface trimmed to avoid an unrelated cross-module Text-const resolution issue tracked separately.
VCS impact for Weft framework: 27 / 30 (90 %) baseline →
29 / 30 (96.7 %).
Added — verum cert-replay multi-backend SMT certificate replay (2026-04-29)
Verum joins the small group of proof assistants where SMT solvers
are external to the trusted computing base. When a solver
claims unsat, Verum doesn't trust the verdict — instead the
solver emits a structured certificate that the kernel re-checks
independently. Multi-backend cross-check --require-consensus
is the @verify(certified) semantics: every available backend
must accept before commit.
Four subcommands:
verum cert-replay replay --backend <B> [--cert FILE | --format F --theory T --conclusion C --body B] [--output ...]— replay one cert through one backend with kernel-only structural baseline.verum cert-replay cross-check [--backend B]… [...] [--require- consensus]— multi-backend agreement gate.verum cert-replay formats / backends [--output ...]— discovery.
Six certificate formats (verum_canonical, smt_native, alethe, lfsc_pattern, open_smt, mathsat) and six replay backends (kernel_only + multiple SMT backends / Verit / OpenSmt / Mathsat).
The kernel_only backend is the trust anchor: validates
integrity hash, recognised format, supported SMT-LIB theory,
non-empty body / conclusion. Any tampering observable. V0 ships
this production-grade plus mock external runners returning
ToolMissing; V1+ swaps in real per-tool wiring without changing
the trait surface or any consumer.
Full guide: Tooling → SMT certificate replay.
Added — verum benchmark continuous head-to-head comparison surface (2026-04-29)
Establishes Verum's quantitative leadership claim with reproducible benchmarks across the proof-assistant landscape (Coq / Lean4 / Isabelle / Agda). Anyone can re-run the suite and verify the numbers — no marketing claims, only verifiable measurements.
Three subcommands:
verum benchmark run --system <S> --suite-name <N> [--theorem T]… [--format plain|json|markdown|csv]— run the suite against a single system.verum benchmark compare [--system S]… --suite-name <N> [--theorem T]… [--format ...]— head-to-head matrix. Without--system, runs all five systems.verum benchmark metrics [--format ...]— list every supported metric with itshigher_is_betterdirection.
Nine canonical metrics: kernel LOC, lines/sec, theorems/sec,
peak RSS, elapsed ms, cross-format exports, tactic coverage,
trust diversification, LLM-tactic acceptance. Each result carries
an optional reproducibility envelope (hash of input corpus + tool
version). Markdown output decorates the leader cell per metric
with ⭐ — direct-paste-able into release announcements.
V0 ships mock runners with canned values reflecting the documented landscape claims (kernel LOC: Verum 5,000 / Coq 200,000 / Lean 50,000; LLM acceptance: Verum 65%, others 0%). V1+ swaps in production runners that actually invoke the foreign tools; the trait surface and CLI / docs / CI scripts are unchanged.
Full guide: Tooling → Continuous benchmarking.
Fixed — LspConfig.show_counterexamples gates extraction (2026-04-29)
Companion closure: LspConfig.show_counterexamples was
forwarded into the validator's ValidatorConfig field but the
Invalid-result branch in validate_impl always ran the full
extract_counterexample extraction pipeline regardless of
policy. When the flag is false, return a stub
CounterexampleData whose violation_reason names the flag
for diagnostic clarity. Useful for minimal status-line UIs.
Fixed — LspConfig.cache_validation_results actually gates the cache (2026-04-29)
The flag was forwarded into the validator's
ValidatorConfig.cache_enabled field but no path in
validate_refinement consulted it — the cache lookup at line
462 and insert at 484 fired regardless of policy.
Wire by reading cache_enabled once at the entry of
validate_refinement (single policy snapshot, no
double-locking) and gating both self.cache.get and
self.cache.insert on it. When disabled, every keystroke
re-runs the full SMT verification pipeline — useful for
debugging flaky proofs or benchmarking the verifier without
cache noise.
Fixed — InterpreterConfig.cbgr_enabled short-circuits validation (2026-04-29)
Closes the inert-defense pattern around the documented opt-out
for VBC interpreter CBGR validation. The field defaulted to
true but no code path consulted it — every dereference paid
the ~15 ns generation-check cost regardless of caller intent.
Wire as an early return at the entry of
validate_cbgr_generation: when false, skip the generation
- epoch-window check entirely. This is the documented trade-off for max-throughput builds where the static escape analyzer has already proven every reference safe.
Added — verum proof-repl live proof REPL with stepwise feedback (2026-04-29)
Non-interactive batch driver for the proof-REPL state machine.
Apply tactics, navigate with undo / redo, request hints, and
emit the proof tree as Graphviz DOT — all from the shell, with
full kernel-grade feedback on every step.
Two subcommands:
verum proof-repl batch --theorem T --goal G [--lemma ...] [--commands FILE] [--cmd LINE]… [--format plain|json]— run a command script.verum proof-repl tree --theorem T --goal G [--lemma ...] [--apply STEP]…— apply a sequence of tactics and emit the resulting proof-tree DOT.
Command-script syntax: one command per line; apply <tactic> (or
bare <tactic> for ergonomics), undo / redo / status /
show-goals / show-context / visualise / hint [N]; # and
blank lines are skipped.
Every command produces a typed response carrying the snapshot of the new state plus the kernel verdict. Rejected steps do NOT mutate the state — the LCF fail-closed contract carries through. Non-zero exit on any kernel rejection (CI-friendly).
Interactive TUI is a future extension; the protocol shape ships today so IDE / CI / shell consumers can integrate against the stable JSON schema.
Full guide: Tooling → Proof REPL.
Fixed — InterpreterConfig.timeout_ms wall-clock budget (2026-04-29)
Closes the inert-defense pattern around the documented VBC interpreter wall-clock budget. The field was declared, defaulted to 0 (no timeout), and never read — adversarial bytecode could spin past the configured budget regardless of caller intent.
Wire at the entry of dispatch_loop_table_with_entry_depth:
when the field is non-zero, capture the deadline as
Instant::now() + Duration::from_millis(timeout_ms). Sample
every 256 instructions (matching the existing cancel-flag
cadence) to bound the cost of Instant::now() calls; on
breach surface as InstructionLimitExceeded with limit = 0.
Reusing the existing error variant keeps caller-side triage
uniform across both budgets.
Fixed — CompilerConfig.{debug_info, optimization_level} forwarded to VBC codegen (2026-04-29)
Two CompilerConfig fields had documented defaults but no
code path forwarded them to VBC codegen. compile() always
called VbcCodegen::new() which used the codegen's own
defaults (debug_info: false, optimization_level: 0)
regardless of caller intent.
Wire by constructing a CodegenConfig template with the
forwarded values and using VbcCodegen::with_config(...)
per-module.
Added — verum llm-tactic LCF-style fail-closed LLM tactic protocol (2026-04-29)
Verum is now the first proof assistant where LLM assistance is guaranteed sound by construction. An LLM may propose tactic sequences for any goal, but the proposal is always re-checked by the kernel before being committed. If the kernel rejects any step, the proposal is discarded and the audit trail records the rejection.
Three subcommands:
verum llm-tactic propose --theorem <T> --goal <G> [--lemma ...] [--hyp ...] [--history ...] [--model <ID>] [--hint <TEXT>] [--persist] [--audit <PATH>] [--format plain|json]— run one protocol round.verum llm-tactic audit-trail [--audit <PATH>] [--format ...]— read every recorded event.verum llm-tactic models [--format ...]— list available adapters.
Every audit-trail event carries the model_id + blake3 prompt
hash + blake3 completion hash, so the proof is reproducible from
the log alone. Four event kinds: LlmInvoked, KernelAccepted,
KernelRejected (with failed_step_index + reason),
ProtocolError.
V0 ships two reference adapters: mock (deterministic, for tests)
and echo (emits a user-supplied --hint verbatim, useful when
you have a pre-computed sequence and want the kernel re-check loop
without an actual model in the loop). Production cloud /
on-device adapters plug in via the same trait surface without CLI
changes.
The fail-closed contract: regardless of what the LLM hallucinates, the kernel verdict is authoritative. No proof body is ever modified without the kernel accepting every step.
Full guide: Tooling → LLM tactic protocol.
Fixed — CommonPipelineConfig.smt_timeout_ms reaches contract phase (2026-04-29)
Closes the inert-defense pattern around the documented
session-level SMT budget. run_common_pipeline invoked
ContractVerificationPhase::new() which fell back to the
phase's own 30 000 ms default regardless of caller intent —
setting smt_timeout_ms = 5000 in the manifest had no effect
on the SMT backend.
Wire by constructing a VerificationConfig with the forwarded
timeout and using with_config(...) instead of new(). Phase
defaults for the other fields (counterexamples, protocols,
etc.) are preserved.
Fixed — SemanticCacheConfig.enable_cross_project gates the persistent fallback (2026-04-29)
enable_cross_project (default false) was documented as
"Whether to enable cross-project sharing" but no code path
consulted it. The three get_*_with_fallback methods (types,
functions, verification results) always consulted the
persistent store when one was attached. Callers wanting strict
per-project caching had no way to opt out.
Wire as an early return at the entry of each fallback method: when disabled, skip the persistent store entirely so the cache behaves as a pure per-process LRU.
Fixed — ContextConfig 5-field wiring on Context::solver (2026-04-29)
Closes the inert-defense pattern around five
ContextConfig fields that had documented defaults but no
code path consulted them in Context::solver(). Only
timeout was forwarded to fresh solver instances; the
other knobs (model generation, unsat-core extraction,
proof generation, memory limit, random seed) were inert at
this construction site.
Wire all five:
unsat_core/model/proof→ Solver Params keys (folded into a singleParamsvalue alongside the timeout — required becauseSolver::set_paramsis destructive).memory_max_sizeandsmt.random_seed→ global the SMT backend params viaset_global_param(these keys must be set at process scope; Solver/Config scopes silently ignore them per the verifier's empirical scope-discipline audit).
Fixed — PortfolioConfig.enabled is now a kill-switch (2026-04-29)
Closes the inert-defense pattern around the portfolio toggle.
BackendChoice::Portfolio unconditionally invoked
solve_portfolio, so a caller that set enabled = false
while leaving BackendChoice::Portfolio in place still
spawned the multi-thread multiple SMT backends race.
Wire as a kill-switch at the entry of solve_portfolio: when
disabled, fall back to solve_auto (single-backend
heuristic-driven routing).
Fixed — ProofGenerationConfig.enable_unsat_cores reaches the SMT backend (2026-04-29)
apply_to_smt_config only forwarded enable_proofs. Wired
unsat_core Config-level the SMT backend param too so every solver
constructed via ProofGenerationConfig::with_config
inherits the policy without per-query re-application.
Fixed — PatternConfig.enable_multi_patterns + track_effectiveness wired (2026-04-29)
Two PatternConfig fields had documented defaults but no
code path consulted them in
PatternGenerator::generate_patterns:
enable_multi_patterns(default true): when enabled and ≥2 simple patterns are generated, fold them into a single multi-pattern viatry_create_multi_pattern. Multi-patterns are more selective (the SMT backend instantiates only when ALL terms appear together), reducing matching work for quantifiers with multiple triggers.track_effectiveness(default true): when disabled, skipstats.record_pattern_generationso callers in hot-path / latency-sensitive contexts don't pay the atomic-counter cost.
Added — Solver-tuning docs + complete config-knob matrix (2026-04-29)
A new operator's manual at
Verification → Solver Tuning
covers every configurable knob in the verification stack
exhaustively: 12 config structs (RefinementConfig,
QEConfig, InterpolationConfig, StaticVerificationConfig,
SmtBackendConfig, SmtBackendConfig, SubsumptionConfig,
BisimulationConfig, SepLogicConfig, UnsatCoreConfig,
ParallelConfig, OptimizerConfig, CacheConfig),
~80 individual fields, defaults, effects, parameter-scope
discipline (Global vs Config vs Solver — empirically verified
per the SMT backend key), copy-paste recipes for latency-sensitive / CI /
deep-debugging / research workflows, and a "destructive
Solver::set_params" gotcha section. The
architecture/smt-integration
page gained a parallel "Configuration knobs" section
(complete matrix of fields + wiring scope) plus a "Solver
parameter scope discipline" section that documents which
of the three the SMT backend scopes honours each key, based on empirical
audit results. The
verum.toml reference gained a
new [verify.solver] schema with sub-tables for every
backend / phase / cache config, plus a recap diagram of how
manifest values reach multiple SMT backends solver params through the
five-layer chain. Closes the request that documentation be
fully self-contained and not require developers to seek
additional resources outside this site.
Added — verum foreign-import foreign-system theorem import (2026-04-29)
Inverse of verum export: reads a Coq / Lean4 / Mizar / Isabelle
source file and emits a Verum .vr skeleton with one
@axiom-bodied declaration per imported theorem, attributed back
to the source via @framework(<system>, "<source>:<line>").
verum foreign-import --from <coq|lean4|mizar|isabelle> <FILE> \
[--out <PATH>] [--format skeleton|json|summary]
Per-system V0 statement-level extractors recognise the canonical
keywords: Coq's Theorem / Lemma / Corollary / Axiom /
Definition, Lean4's theorem / lemma / axiom / def,
Mizar's theorem / definition, Isabelle's theorem / lemma /
axiomatization. Comments are stripped before extraction.
Three output formats: skeleton (default — a copy-paste-able
.vr source), json (structured payload for tooling pipelines),
summary (human-readable count + name list).
Bidirectional reproducibility: a theorem proved in Verum can be exported to Lean; a theorem proved in Lean can be imported to Verum and (re-)proved by Verum's kernel. Disagreement at any step is a bug somewhere in the chain.
Full guide: Tooling → Foreign-system import.
Added — verum doc-render auto-paper generator (2026-04-29)
A Verum corpus IS a formal proof AND a paper draft. Pre-this-tool a
project had to maintain paper.tex alongside the .vr corpus —
two sources of truth, manual sync risk. verum doc-render makes
the corpus the single source of truth: walks every public
@theorem / @lemma / @corollary / @axiom declaration,
projects each into a typed DocItem, and emits Markdown / LaTeX /
HTML directly from the parsed AST + docstrings.
Three subcommands:
verum doc-render render [--format md|latex|html] [--out PATH] [--public]verum doc-render graph [--format dot|json] [--public]verum doc-render check-refs [--format plain|json] [--public]
Reproducibility envelope: every rendered statement carries an
optional closure hash (from the closure-cache) so readers can
re-verify via verum cache-closure decide.
Future per-format adapters (LaTeX-with-proof-tree-collapse, HTML-with-MathJax, Markdown-with-Mermaid-graphs) plug in without changing the user-facing CLI.
Full guide: Tooling → Auto-paper generator.
Added — closure-cache wired into verum verify pipeline (2026-04-29)
The closure-cache is now wired into the theorem-proof pipeline. Theorem proofs whose closure hash + Ok-verdict are already cached are skipped without invoking the SMT/kernel re-check.
CLI flags:
verum verify --closure-cache— opt in.verum verify --closure-cache-root <PATH>— override default<input.parent>/target/.verum_cache/closure-hashes/.
Verify run summary now includes a cache-hit-ratio line:
Closure cache: 138 hit(s), 4 miss(es), 97.2% hit-ratio
The [verify] block in verum.toml accepts the same knobs:
[verify]
closure_cache_enabled = true
closure_cache_root = "/nfs/verum-cache/main"
CLI flags always override the manifest. Persist failures don't poison the verdict — a cache write error is reported but the freshly-computed verdict is still returned authoritatively.
Added — closure-hash incremental verification cache (2026-04-29)
Per-theorem (fingerprint → verdict) cache enabling skip-mode
verification. The closure fingerprint is a blake3 hash over
(kernel_version + theorem signature + proof body +
sorted+deduped @framework citations). Kernel-version drift
invalidates ALL caches unconditionally — the trust boundary has
shifted.
Cache decisions are typed: a recheck always cites a specific
cause (no_cache_entry / fingerprint_mismatch /
kernel_version_changed / previous_verdict_failed). No silent
fall-through.
Inspector / control surface: verum cache-closure {stat,list,get,clear,decide} — five subcommands giving IDE / CI
programmatic access.
Disk format: one JSON file per theorem under
target/.verum_cache/closure-hashes/.
Full guide: Tooling → Incremental cache.
Added — industrial-grade tactic combinator catalogue (2026-04-29)
Single source of truth for Verum's 15 canonical tactic combinators
(skip / fail / seq / orelse / repeat / repeat_n / try
/ solve / first_of / all_goals / index_focus /
named_focus / per_goal_split / have / apply_with) and their
12 algebraic laws.
CLI surface:
verum tactic list [--category C] [--format plain|json]verum tactic explain <name> [--format plain|json]verum tactic laws [--format plain|json]
Every catalogue entry carries a stable doc_anchor consumed by the
auto-paper generator. Five categories (identity / composition /
control / focus / forward) used for output grouping.
Stdlib extension: core.proof.tactics.combinators gains solve,
case_focus, per_goal_split; new core.proof.tactics.forward
ships have / apply_with for Lean / SSReflect-style forward
chaining.
Composite catalogues (cubical / stochastic / domain-specific) plug in alongside the default catalogue without forking.
Full guide: Tooling → Tactic catalogue.
Added — verum proof-repair structured repair-suggestions CLI (2026-04-29)
Surfaces ranked drop-in code-snippet repairs for nine typed failure kinds (refine-depth / positivity / universe / fwax-not-prop / adjunction / type-mismatch / unbound-name / apply-mismatch / tactic-open).
verum proof-repair --kind <K> [--field key=value]…
[--max <N>] [--format plain|json]
Plain output renders headline + ranked suggestions with rationale + applicability + doc-link; JSON output suitable for IDE code-action emission.
Full guide: Tooling → Proof repair.
Added — verum proof-draft ranked tactic-suggestion CLI (2026-04-29)
Given a theorem name + focused goal + available lemmas, emits ranked next-step tactic suggestions.
verum proof-draft --theorem <T> --goal <G>
[--lemma name:::signature[:::lineage]]…
[--max <N>] [--format plain|json]
Drives IDE / REPL hover panels and IDE completion of obligation next-steps. Full guide: Tooling → Proof drafting.
Added — verum verify --ladder 13-strategy ladder dispatcher (2026-04-29)
Per-theorem @verify(strategy) annotations route through the
typed 13-strategy dispatcher; emits per-theorem verdicts (Closed /
Open / DispatchPending / Timeout) plus a totals summary.
verum verify --ladder [--ladder-format plain|json]
DispatchPending is advisory (today's reference implementation ships end-to-end backends for 2 of 13 strategies — Runtime + Static); Open / Timeout produce non-zero exit. The dispatcher enforces strict ν-monotonicity along the ladder backbone — implementing a stricter strategy without implementing every coarser one is caught at audit time.
Full surface in Verification → CLI workflow → Ladder.
Fixed — verum.lsp.validationMode = "complete" honours configured timeout (2026-04-29)
Closes the inert-defense pattern around the LSP
validation_mode Complete variant. The variant was
documented as "Unlimited — reserved for CI/CD, not used by
the LSP server directly", but apply_config silently
normalised it to Thorough at the validator boundary —
clients that set verum.lsp.validationMode = "complete" got
the 1s Thorough timeout instead of the documented
unlimited-latency behaviour.
Wired via a new Complete variant on the validator's own
ValidationMode enum mirroring the LspConfig variant. The
timeout-derivation match recognises Complete and falls back
to the configured cfg.smt_timeout (typically tens of
seconds), preserving the documented contract while still
honouring whatever budget the client did set. The Complete
variant is intentionally bounded by smt_timeout rather than
truly unlimited — clients running CI/CD don't want a
malformed proof to deadlock their pipeline forever.
Fixed — verum update --workspace includes build-dependencies (2026-04-29)
Closes the inert-defense pattern around the workspace-update
flag. UpdateOptions.workspace landed on the options struct
from the CLI --workspace flag but was never read — so
verum update --workspace looked identical to plain
verum update, defeating the documented opt-in.
Wired in update_all_cogs after the dependencies + dev-
dependencies collection: when set, additionally include
manifest.build_dependencies (the build-time-only dep group
that's normally invisible to the update walk because runtime
execution doesn't see it). De-duped against the existing
collection.
Mirrors the established verum tree --all-features semantics
and matches the cargo update --workspace convention:
workspace-mode opts INTO updating every declared dep group,
not just runtime + dev defaults.
Fixed — LspConfig.smt_solver flows into diagnostic tags (2026-04-29)
Closes the inert-defense pattern around the
verum.lsp.smtSolver LSP setting. The choice was parsed from
the client's initializationOptions and surfaced in the
initialize handler's tracing log line, but never reached the
validator — clients that set verum.lsp.smtSolver = "smt-backend"
saw smt_solver: the SMT backend in every RefinementDiagnostic regardless
of their stance.
Wired via a new smt_solver: SmtSolver field on
ValidatorConfig populated by apply_config from the
LspConfig.SmtSolverChoice enum: the SMT backend→the SMT backend, Cvc5→the SMT backend,
Auto→the SMT backend (validator's preferred default; a future capability-
router enhancement could route Auto through
verum_smt::backend_switcher for per-goal selection).
build_diagnostic now consults the configured stance instead
of hardcoding the SMT backend, so every diagnostic carries the actual
backend tag — embedders that display "verified by the SMT backend in 50ms"
UI annotations now show the user's actual configured solver.
Fixed — ParallelConfig.race_mode controls worker termination (2026-04-29)
Closes the inert-defense pattern around the parallel SMT
solver's race-mode gate. ParallelConfig.race_mode defaulted
to true and was asserted in a test, but no code path
consulted it — worker termination after first-result was
unconditional, so embedders running consensus-based portfolios
(where multiple workers must agree before the result is
accepted) had to terminate-then-restart instead of letting
workers complete naturally.
Wired in the executor's post-result termination sequence: when the gate is true (default), the existing "first-result wins, kill the rest" behaviour is preserved. When false, workers are NOT killed, allowing the embedder to drain remaining verdicts via a follow-up call.
A future enhancement could thread race_mode into the wait loop itself to switch between first-result and all-results aggregation; this commit lands the gate at the termination site so consensus-style portfolios can drain correctly without restructuring the wait loop.
Fixed — TargetConfig.gpu_targets exposed via accessors (2026-04-29)
Closes the inert-defense pattern around the per-target GPU
selection. TargetConfig.gpu_targets: Vec<GpuTarget> was set
by the with_gpu(targets) constructor and asserted in tests,
but no code path queried the list — only the derived has_gpu
boolean was consulted by routing. Downstream codegen layers
had to re-derive the preferred target from environment
variables or hardcode CUDA, defeating the documented
manifest-driven target selection.
Wired via two accessors on TargetConfig:
preferred_gpu_target() -> Option<&GpuTarget>— returns the first entry ingpu_targetswhen GPU compilation is available. Selection policy is first-declared (manifest order is authoritative — embedders that need a different priority reorder the list at construction).supports_target(&GpuTarget) -> bool— discriminator-only match for "is this GPU class available in the configured target list". SM versions / GFX versions are capability filters consumed at codegen time, not routing-time presence checks; surfacing the discriminator semantic explicitly prevents callers from over-constraining the query.
Fixed — LtoConfig.pic reaches ThinLtoCodegen (2026-04-29)
Closes the inert-defense pattern around the ThinLTO PIC-model
gate. LtoConfig.pic defaulted to true and was honoured by
FullLtoCodegen via set_pic_model, but ThinLtoCodegen's
apply_config had a comment-only acknowledgment line
(let _ = config.pic) claiming "PIC model is determined by the
input bitcode modules" — incorrect; LLVM's C API exposes
thinlto_codegen_set_pic_model for per-codegen overrides.
Wired via new set_pic_model(&self, bool) method on
ThinLtoCodegen wrapping the C API. ThinLtoCodegen::apply_config
now calls it after the cache settings, replacing the inert
acknowledgment line.
User-facing impact: ThinLTO builds (the default LTO mode for
most release configurations) now respect the manifest's pic
stance instead of silently using whatever the input bitcode
modules were compiled with. For shared-library targets, this
ensures ThinLTO doesn't accidentally produce position-dependent
code that breaks ASLR / dlopen.
Fixed — LtoConfig.internalize reaches the LLVM full-LTO codegen (2026-04-29)
Closes the inert-defense pattern around the LLVM internalize gate.
LtoConfig.internalize defaulted to true and was asserted in
config tests, but FullLtoCodegen::apply_config set CPU + debug-
model + PIC-model and silently ignored the internalize stance,
so the LLVM default was always used regardless of the manifest.
Wired via new set_internalize(&self, bool) method on
FullLtoCodegen wrapping lto_codegen_set_should_internalize
(the canonical LLVM C API for this knob). apply_config now
calls it after the existing CPU / debug / PIC settings, so every
full-LTO invocation inherits the configured stance.
Internalization converts external linkage to internal linkage
for symbols that aren't part of the public API — unlocks
aggressive inlining and dead-code elimination on functions that
would otherwise be considered linker-visible. Embedders shipping
shared libraries that need to preserve external linkage opt out
via internalize: false in the manifest.
Fixed — 7 LinkConfig fields wired into the LTO link path (2026-04-29)
Closes seven inert-defense patterns at the LTO link boundary.
Seven LinkConfig fields were consumed by the standard (non-LTO)
link path but silently dropped in link_with_lto — setting any
of them on a verum build --release build (which typically
enables LTO) had zero observable effect on the linked binary:
strip_debug— full debug-section size in LTO outputs.entry_point— manifest's configured entry ignored under release; the linker's default entry was used.static_link— static-link manifests under release / LTO produced dynamically-linked outputs.exports— exported symbols configured in the manifest were dropped from LTO outputs (broke shared-library surfaces).rpaths— runtime library search paths dropped under LTO.linker_script— custom linker scripts silently ignored.extra_args— raw linker flags fed via the manifest or-C link-arg=were dropped under LTO.verbose—--verboselinker output suppressed under LTO, defeating the diagnostic purpose for users debugging release-build link failures.
Wired symmetrically with the standard link path: every field
the standard branch consumes now reaches the LTO branch too,
with the same precedence rules. The standard path remains the
authoritative reference; new fields added to LinkConfig should
be wired into BOTH branches at the same time to keep them
convergent.
Fixed — GpuPassConfig.enable_tensor_cores reaches the pipeline (2026-04-29)
Closes the inert-defense pattern around the GPU pipeline's
NVIDIA tensor-core gate. GpuPassConfig.enable_tensor_cores
defaulted to true for CUDA and false for ROCm, asserted in
preset tests, but no production code path consulted it.
Wired via two surfaces:
tensor_cores_enabled() -> boolaccessor onGpuPassPipelineso kernel-level codegen layers can branch on the stance without re-reading the pipeline config.- Structured
tracing::debug!emission inside the CUDA target-lowering branch: when the flag is on AND the target is CUDA, the pipeline logs that tensor-core utilization is enabled. Lets downstream observers correlate kernel performance with the policy.
Important note: the actual mma.* PTX instruction emission
depends on the kernel being structured for tensor-core use
(matrix tile sizing, fragment loads, mma.* intrinsics) — that's
the kernel codegen's responsibility, not this pass pipeline's.
The wiring documents the configured stance and surfaces it to
downstream layers.
Fixed — PassConfig.enable_standard_opts + debug_ir_printing are load-bearing (2026-04-29)
Closes two inert-defense patterns in
verum_codegen::mlir::passes::pipeline::PassConfig. Both fields
were exposed in default-config presets and asserted in pin tests
but no code path consulted them.
enable_standard_opts (default true) — wired as a master
umbrella over both early AND late MLIR optimization phases. The
umbrella is the load-bearing single off-switch for "skip all
standard MLIR optimizations" — Verum-domain passes (CBGR /
context-mono / refinement) still run if their own flags are on.
The per-phase enable_early_opts / enable_late_opts give
finer-grained control beneath the umbrella.
debug_ir_printing (default false) — wired in run_passes to
dump the module IR via tracing::debug! at four points: before
any pass runs (pristine input), after early-opts, after late-opts,
and after llvm-lowering. Lets debuggers / custom pipeline
harnesses see exactly what each phase produced. Default-off keeps
the production trace stream quiet.
Fixed — CacheConfig.distributed_cache auto-installs the backend (2026-04-29)
Closes the inert-defense pattern around the verification cache's
distributed-backend gate. CacheConfig.distributed_cache was
exposed via the with_distributed_cache(config) builder but no
code path consulted it during cache construction — so configuring
a distributed backend in the manifest had zero observable effect.
Users had to additionally call with_distributed(...) with a
hand-built backend, defeating the documented "configure once"
contract.
VerificationCache::with_config now auto-builds the backend from
the configured stance via a new build_distributed_from_config
helper. The translation logic between the local TrustLevel
variants and the underlying distributed_cache module's variants
lives there; in particular the local SignaturesAndExpiry variant
downgrades to the module's Signatures (the lower bound of the
requested policy) — the cache's own ttl field handles the
expiry side independently.
On backend-construction failure, the path logs a tracing warning
and falls back to local-only cache rather than failing the
constructor — keeps the cache usable on operational issues while
surfacing the diagnostic. Callers that need to surface the
failure explicitly can still construct the backend themselves and
pass it via with_distributed.
Fixed — SmtBackendConfig: 5 inert fields wired into the SMT backend global params (2026-04-29)
Closes five inert-defense patterns at once in verum_smt::backend::SmtBackendConfig.
All five fields landed on the config struct, were exposed in
default-config presets, and asserted in pin tests, but no code
path forwarded them to the SMT backend — toggling any of them had zero
observable effect on the solver.
minimize_cores(default true) →smt.core.minimizethe SMT backend global param. Pre-fix the field defaulted to true (claiming default minimization) but the SMT backend always returned non-minimized cores.enable_mbqi(default true) →smt.mbqi. Disabling forces the solver onto pattern-based / E-matching strategies only.enable_patterns(default true) →smt.ematching. Disabling forces the solver onto MBQI-only when quantifiers are present.num_workers(defaultnum_cpus::get().max(4)) →parallel.enable+parallel.threads.max. Zero is treated as "let the SMT backend decide" — leaving the parallel-pool params untouched preserves the documented sentinel without poisoning the global state with a bogus zero-worker setting.enable_interpolation→ exposed via new publicinterpolation_enabled()accessor for the higher-levelverum_smt::interpolationlayer to consult (the SMT backend's built-in interpolation API was removed in modern releases).
Wired in with_config so every Solver constructed inside the
scoped run inherits the policy — mirrors the existing
memory_limit_mb and random_seed wiring pattern.
Fixed — verum publish --verify-proofs blocks on failed proofs (2026-04-29)
Closes the inert-defense pattern around the publish verify-proofs
gate. PublishOptions.verify_proofs landed on the options struct
from the CLI --verify-proofs flag but no code path consulted it
— verum publish --verify-proofs would happily upload a package
whose proof bundle contained ProofStatus::Failed entries,
defeating the documented enforcement contract.
Wired via new check_verify_proofs_gate(enforce, proofs) helper
called after metadata generation. When the gate is set AND the
proof bundle contains any Failed entries, the publish is blocked
with a typed CliError::Custom that names every failed function
so publishers can fix the offending items before retry.
The gate enforces "no failed proofs", NOT "must have proofs" —
a package with zero @verify items still publishes cleanly under
--verify-proofs. The stricter "must have proofs" policy would
deserve its own separate flag rather than silently subsuming under
this check.
Fixed — verum tree --all-features shows build-dependencies (2026-04-29)
Closes the inert-defense pattern around the verum tree
all-features flag. TreeOptions.all_features landed on the
options struct from the CLI --all-features flag but was never
read — so verum tree --all-features looked identical to plain
verum tree, defeating the documented opt-in.
Wired in tree() after the dev-dependencies block: when set,
additionally print a Build Dependencies: section using
manifest.build_dependencies (the build-time-only dep group
currently invisible in the default tree because runtime execution
doesn't see it). Mirrors cargo tree --all-features semantics —
the flag opts INTO showing every declared dep group, not just the
runtime + dev defaults.
Fixed — verum bench --no-color actually suppresses ANSI output (2026-04-29)
Closes the inert-defense pattern around the verum bench
no-color flag. The field landed on BenchOptions from the CLI
--no-color flag but was never read — so verum bench --no-color
still emitted ANSI escapes, breaking output captured in CI logs
that didn't strip them.
Wired at the start of execute: when the flag is set, call
colored::control::set_override(false) to suppress all ANSI-styled
output globally for the duration of the bench run. The colored
crate is already a dependency for the existing styled tables, so
the wiring is a single call (no new dependencies, no API change).
Fixed — verum test --release and --verify reach the test compiler (2026-04-29)
Closes two inert-defense patterns at the verum test CLI →
test-compiler boundary. Both flags landed on TestOptions but
were dropped before reaching the test compiler — setting
--release or --verify <mode> had zero observable effect on
the test binary.
--release (default off) — wired in run_test_aot to set
CompilerOptions.optimization_level = 3 when true (otherwise 0,
matching the per-test compilation default). Mirrors the
verum build --release semantics so production-mode tests
exercise the optimized code path.
--verify <mode> — wired through a new
TestRunCfg.verify_mode_override: Option<VerifyMode> field. The
string-form CLI flag runtime / static / proof (case-
insensitive) maps to the typed enum at the cfg construction site.
static is accepted as a synonym for proof (matches the
user-facing docs that promise both spellings reach the SMT-backed
verifier). Unrecognised values fall back to the per-test default
rather than failing at this layer — keeps CI tolerant of typos in
invocation strings.
Fixed — ContextConfig.simplify wired into a public assert path (2026-04-29)
Closes the inert-defense pattern around the verum_smt::context
pre-solve simplifier gate. The field defaulted to true and was
documented as "Enable simplification before solving" but no code
path consulted it — every assertion went straight to the solver
regardless of the configured stance.
Wired via two new methods on Context:
simplify_enabled() -> bool— public read of the configured stance.assert(&Solver, &Bool)— opt-in assert path that runsformula.simplify()(the SMT backend's per-AST simplifier) when the gate is set, then asserts the simplified form. Default-on means callers routing through this method get pre-solve simplification for free.
The wiring is opt-in by call site, not retroactively applied
to direct solver.assert(&formula) callers — flipping every site
at once would invalidate existing performance baselines. New call
sites should route through Context::assert to inherit the policy.
Fixed — CLI strip / static-link flags reach the linker (2026-04-29)
Closes three inert-defense patterns at the CLI → linker boundary.
CompilerOptions.strip_symbols, strip_debug, and static_link
were exposed via builders + landed in the parsed options struct
but were NEVER propagated to the LinkingConfig that the linker
phase actually consumes. Setting --strip-symbols, --strip-debug,
or --static-link on the CLI had zero observable effect on the
linked binary — the linker silently used whatever the manifest
had configured.
Wired via new apply_cli_link_overrides free function called
immediately after the manifest-loaded LinkingConfig is
constructed (mirrors the existing --lto precedence at the same
site).
The merge is additive by design: a CLI flag can opt INTO a
stricter stance (strip more, link statically) but cannot turn off
a stance the manifest already enabled. This is load-bearing — the
manifest is the per-project default and the CLI is per-invocation;
allowing the CLI to flip a stance OFF would let verum build
accidentally undo a signed manifest's strip / static-link policy.
Fixed — ReplConfig.verbose + timeout_seconds are load-bearing (2026-04-29)
Closes two inert-defense patterns at the JIT-REPL session boundary. Both fields were documented but no code path consulted them; toggling either had zero observable effect.
verbose (default false) — wired in ReplSession::eval to emit
a structured tracing event on entry naming the session id, eval
number, and input byte count. Default-off keeps the production
hot path free of tracing overhead.
timeout_seconds (default 0 = no timeout) — wired at the start
of eval: when nonzero and created_at.elapsed() exceeds the
configured budget, eval rejects with a typed
MlirError::ReplError whose message names the field, the
configured budget, and the actual elapsed time so callers can
attribute the failure correctly. The "0 = no timeout" sentinel
preserves the existing unbounded behaviour for default sessions.
Fixed — Enterprise AccessControl.require_signature enforces policy (2026-04-29)
Closes the inert-defense pattern around the enterprise signature-
verification gate. The field was documented as "Require signature
verification", parsed from enterprise.toml, and asserted in
default-construction tests, but no code path consulted it.
Enterprises that set the flag in their config would still install
unsigned cogs because EnterpriseClient::is_cog_allowed only
checked the allow / deny lists.
Wired via two new methods on EnterpriseClient:
requires_signature() -> bool— public read accessor surfacing the configured stance so install / publish flows can branch on the policy without re-readingEnterpriseConfiginternals.is_cog_allowed_with_signature(cog_name, has_valid_signature) -> bool— combined check that runs the existing name-only check first and then enforces the signature requirement when the policy demands it.
The signature gate is ADDITIVE: it doesn't bypass the deny list,
and an unsigned cog that fails the name-only check is still
rejected without consulting signature state. The pre-existing
is_cog_allowed(&str) continues to work for callers that don't
have signature info yet — they call requires_signature() to
decide whether to look up signature state before proceeding.
Fixed — ProofGenerationConfig.minimize_unsat_cores + extraction_timeout_ms reach the the SMT backend Solver (2026-04-29)
Closes two inert-defense patterns at the proof-extraction boundary. Both fields were documented the SMT backend controls but no code path forwarded them to the Solver — toggling either had zero observable effect on extraction behaviour.
minimize_unsat_cores is the smt.core.minimize the SMT backend param —
Solver-level (not Config-level) so it can't ride on the existing
apply_to_smt_config path. When true, the solver runs additional
minimization on the unsat core before returning it, producing
tighter explanations at extra solver cost.
extraction_timeout_ms is the timeout the SMT backend param. The "0 = no
timeout" semantic is preserved by OMITTING the param entirely when
the field is zero — the SMT backend interprets timeout=0 as "fire immediately"
on some param paths, which would defeat the documented unbounded
behaviour. Saturating clamp via min(u32::MAX as u64) as u32
prevents silent overflow on hostile / pathological config (~49
days of milliseconds, well past anything the SMT backend would honour).
Wired via new apply_to_smt_solver(&Solver) method, parallel to
the existing apply_to_smt_config(&mut Config). Callers running
proof-extraction work invoke this on the Solver they're about to
query so the per-call resource budget actually reaches the solver.
Fixed — ValueTrackingConfig is fully load-bearing (2026-04-29)
Closes four inert-defense patterns at once — the entire
ValueTrackingConfig struct in verum_cbgr::value_tracking was
documentation-only. ValuePropagator::new ignored it; the
track_concrete_values consumer ran with hard-coded behaviour;
setting any field had zero observable effect.
ValuePropagator now owns the config (new with_config(config)
constructor + config() accessor) and threads three independent
per-domain gates through every transfer function:
enable_constant_propagation— gates concrete-value writes inpropagate_constant, the constant fast-path insidepropagate_binop, and the merge-into-concrete path inpropagate_phi.enable_range_analysis— gates the range-refinement branch inpropagate_binop.enable_symbolic_execution— gates symbolic-mirror writes inpropagate_constant,propagate_binop, andpropagate_phi.
Independence is load-bearing: callers can opt out of any single domain (e.g. disable symbolic execution to halve memory traffic when only constant folding is needed) without losing the others.
max_iterations flows through track_concrete_values_with_config
(new entry-point) to cap the worklist walk on pathological CFGs;
the existing track_concrete_values() keeps working unchanged via
delegation to the configured form with ValueTrackingConfig::default().
Fixed — MonoPhaseConfig.use_stdlib + num_threads are load-bearing (2026-04-29)
Closes two inert-defense patterns at the monomorphization-phase boundary.
use_stdlib (default true) — documented as gating the stdlib
precompiled-specialization lookup, but MonomorphizationPhase::execute
always installed the stdlib resolver if one was provided via
with_core. Setting use_stdlib = false had no observable effect.
Wired at the resolver-installation site: false now skips the
with_core call even when a stdlib module is present, letting
embedders measure the precompiled-cache hit cost or force every
specialization through the user pipeline (useful for differential
testing of the specializer against the cache).
num_threads (default 0 = auto) — documented as the parallel
specialization worker count, but the parallel path always used
rayon's global default pool. Now a bespoke
rayon::ThreadPoolBuilder is constructed when nonzero and the
parallel iterator runs inside pool.install(...) so worker spawns
honour the configured count. Zero preserves the global-pool path.
The bespoke pool is the right knob for CI workers that limit
cross-build interference, measurement runs that want fixed worker
counts, and embedders that share rayon with other systems.
A new MonoPhaseError::ParallelExecution(String) variant carries
the rayon error message + configured worker count for triage when
ThreadPoolBuilder construction fails.
Fixed — CodegenConfig.validate runs the structural validator (2026-04-29)
Closes the inert-defense pattern around the codegen-time VBC
validator gate. The field defaulted to true and exposed
with_validation() builder, but no code path consulted it — so
the documented "validate the freshly-built module before returning"
contract was a no-op. A codegen regression that produced malformed
VBC would slip through here unchecked, surfacing far later as an
interpreter panic or serializer error far from the codegen site.
finalize_module now invokes validate::validate_module(&module)
(strict mode) after build_module(). Validator failures surface as
CodegenError::internal carrying the module name and the underlying
diagnostic text. Default-on means every codegen pipeline now gets the
structural-invariant safety net for free; the gate honours an opt-out
(config.validate = false) for callers that have already validated
upstream and want the zero-cost hot path back.
Fixed — CrashReporterConfig.app_name flows into every report surface (2026-04-29)
Closes the inert-defense pattern around the documented embedder-
rebrand vector. The field was documented as "Human-readable
application name" and the diagnostics docs explicitly promised that
"Downstream tools that embed the compiler should install with an
app_name ... appropriate to them so their crash surfaces point users
at the right bug tracker." But no code read the field — embedders
that set app_name = "myapp" would still see "Verum crash report" in
the .log header, "verum:" on stderr, and "the Verum compiler" in the
closing prose, leaving the docs claim a lie.
The configured app_name now flows into five user-facing surfaces:
=== {app_name_titlecased} crash report ===log header (first letter title-cased automatically —myapp→Myapp).Build: {app_name} {ver} (...)line of the human report (verbatim casing — matchesverum --versionstyle).app_namefield in the JSON envelope'senvironmentblock (additive, schema_version stays at 1).{app_name}: internal compiler error...stderr prefix.run \{app_name} diagnose bundle`` hint shown after a crash.
Single config override now rebrands the whole reporter without touching any rendering code.
Added — ExpansionConfig.debug_bindings records the macro-expansion trace (2026-04-29)
Closes the inert-defense pattern around the macro-expander binding
tracker. The field was documented as "Whether to track all bindings
for debugging" with default false, but no code path consulted it —
the documented hook for tracing macro expansion was a no-op.
When set, the expander records six choke-point events
chronologically: EnterQuote / Binding(BindingKind) / Reference /
Splice / Lift / ExitQuote. Each event carries the binding kind
(when applicable), the identifier name, the source span, and the
quote nesting depth at the time of the event. Two public accessors
expose the trace: debug_bindings_log() -> &List<DebugBindingEvent>
(borrow) and take_debug_bindings_log() -> List<DebugBindingEvent>
(zero-clone drain). Default-off means production callers pay zero
allocation; opt-in tooling (LSP-side macro inspectors, custom
expansion harnesses, integration tests) gets a full reconstruction
of the macro-expansion timeline.
Fixed — ShapeConfig.max_rank enforces the rank ceiling on every verify_* path (2026-04-29)
Closes the inert-defense pattern around the per-tensor rank ceiling.
The field was documented as the rank limit but never read by the
verifier — an analyzer initialised with max_rank = 4 would happily
verify a rank-100 reshape, defeating the purpose of bounding the
static-analysis budget.
A new check_rank_bound helper now runs at the entry of every
public verify_* operation: matmul (both operands), elementwise
(both), broadcast (both + result rank check), reduction, transpose,
reshape (input + new_shape), and concat (every input). On overrun the
operation surfaces ShapeError::InvalidOperation with the
"rank ≤ max_rank (N)" requirement string and "rank K" actual,
naming the offending operation. The check is O(1) so tightening
the cap costs nothing at verification time.
Fixed — ReplConfig.max_display_size actually truncates eval output (2026-04-29)
Closes the inert-defense pattern around the REPL output budget. The field defaulted to 4096 but no code path consulted it — a pathological eval (1 GB tensor stringification, loop-debugged trace) would blow up REPL stdout regardless of caller intent.
EvalResult::display now truncates value text to the configured
number of characters (Unicode-safe via chars().take(N), not
byte slicing) with a …(truncated, N total chars) trailer.
Char-boundary safety is load-bearing: naive byte slicing on
mixed-CJK / emoji values would panic on a multi-byte boundary.
Fixed — OptimizerConfig.incremental gates push/pop scope (2026-04-29)
Closes the inert-defense pattern around the incremental-solving
toggle on SmtOptimizer. The flag was documented as "Enable
incremental solving" with default true, but push / pop
always manipulated the underlying solver scope regardless of
configuration — toggling the flag had zero observable effect.
Both methods now consult self.config.incremental: when the
flag is off, scope manipulation is a no-op so callers that
build the optimizer in non-incremental mode can't accidentally
rely on push/pop semantics that aren't active. The pair stays
balanced because both sides are gated identically.
A new is_incremental() accessor lets callers branch on the
policy without re-reading the config struct.
Fixed — UnsatCoreConfig.timeout_ms reaches the SMT backend (2026-04-29)
Closes the inert-defense pattern around the documented 10 s core-extraction timeout. The field was set on the config struct but no code path forwarded it to the SMT backend — hostile or pathological assertion sets could spin unbounded during core minimization.
create_tracked_solver now folds the timeout into the same
Params value that already sets unsat_core = true. Both
options must arrive together because Solver::set_params
replaces the entire param set; two separate calls would erase
the first one. Saturates at u32::MAX since Params::set_u32
is the exposed type.
Fixed — ParallelConfig.enable_sharing gates lemma exchange (2026-04-29)
Closes the inert-defense pattern around the result-sharing
toggle on the parallel SMT solver. The flag was documented as
"Enable result sharing between workers" with default true,
but only the more specific enable_lemma_exchange flag gated
the actual exchange machinery. Setting enable_sharing = false
had no observable effect.
Wire as the broader gate: lemma exchange is ONE form of
sharing, so disabling enable_sharing must also disable the
exchange channel and thread regardless of
enable_lemma_exchange. Both flags must be true for sharing
to be active.
Callers that disable sharing for memory or determinism reasons (e.g. reproducible workers, no cross-talk) now get the documented behaviour instead of silent lemma exchange.
Fixed — ProverConfig.verbose emits structured tactic traces (2026-04-29)
Closes the inert-defense pattern around the verbose-output
toggle on the interactive prover. The field was documented
as "Verbose output" with a default of false, but no code
path consulted it — flipping the flag had no observable
effect.
InteractiveProver::step now emits a tracing::info! line
for every tactic application when verbose = true, naming
the tactic and the goal index out of the open-goal stack.
Useful for debugging stuck proofs interactively.
Fixed — JitConfig.max_cache_size actually bounds the JIT function cache (2026-04-29)
Closes the inert-defense pattern around the documented JIT
function cache size limit. The field was set to 1024 by
default and surfaced via the builder API, but
JitEngine::get_function's insert path didn't consult it —
the cache could grow without bound across long-running
JIT sessions.
The insert path now evicts the oldest entry (by
compiled_at) when the cache would exceed the configured
cap. The bound is a soft cap (DashMap's per-shard locking
means the size check + insert is not strictly atomic), but
long-running sessions stay well below the documented ceiling
instead of growing unboundedly.
A guard against max_cache_size = 0 keeps the cache fully
disabled rather than evicting on every insert, matching the
"no caching" semantic callers would expect from setting the
cap to zero.
Fixed — HotReloadConfig 3-field wiring: enable_migration + max_replacement_time_us + atomic_replacement (2026-04-29)
Closes three inert-defense patterns on HotReloadConfig. The
fields had documented defaults but no code path consulted
them:
enable_migration(defaulttrue) —register_migrationnow rejects when disabled with a typedHotCodeErrorwhose diagnostic names the flag. Callers can detect the policy and fall back to a different upgrade strategy.max_replacement_time_us(default 1 000 000 µs = 1 s) — thereplacepath now compares the elapsed replacement time against the configured ceiling. A breach surfaces as atracing::warn!so callers can react (e.g. by callingrollback); the replacement itself isn't undone since the new code is already live and rolling back mid-call is its own hazard.atomic_replacement(defaulttrue) — gates the global cross-function replacement lock. Atomic mode (the default) serialises every replacement so concurrent callers always observe a consistent function pointer; non-atomic mode trades that guarantee for less head-of-line blocking when many independent functions reload in parallel. The per-function RwLock still serialises mutators of the same hot-fn, so non-atomic mode is safe for distinct function names — only cross-function ordering is relaxed.
Five new pin tests cover (a) the documented defaults, (b) the register-when-enabled success path, (c) the register-when-disabled rejection path with diagnostic-name assertion, and (d) construction round-trips for each flag.
Fixed — VerificationConfig.mode reaches the unannotated-function branch (2026-04-29)
Closes the inert-defense pattern around the phase-level
VerifyMode knob on the contract-verification phase. The
field was documented as the default verification strategy for
unannotated functions, but no code path consulted it: setting
config.mode = Runtime would still run SMT for every function
that lacked an explicit @verify(...) attribute.
The unannotated-function branch in verify_function_contract
now consults self.config.mode:
Runtime— skip SMT entirely (mirror of@verify(runtime)), recordfunctions_skipped_smtfor the report.Proof/Auto— proceed with the SMT path (existing behaviour).
Two pin tests cover (a) the documented default of Auto and
(b) the round-trip of every variant through the phase config.
Fixed — SmtBackendConfig 3-field wiring: preprocessing, quantifier_mode, verbosity (2026-04-29)
Closes three inert-defense patterns on SmtBackendConfig. The
fields had documented defaults but no code path forwarded
them to the underlying the SMT backend solver:
preprocessing(defaulttrue) — now sets the SMT backend'spreprocess-onlyoption (false → run full pipeline, true → stop after preprocessing).quantifier_mode(defaultAuto) —Autoleaves the SMT backend's heuristic in place; the four named modes (None,EMatching,CEGQI,MBQI) pin a single strategy via thequant-modeoption.verbosity(default0, range 0-5) — sets the SMT backend'sverbosityoption directly. Saturates at 5 for higher inputs.
Four pin tests cover the documented defaults plus the exhaustive enum / extreme-value round-trips.
Fixed — SepLogicConfig.enable_frame_inference gates infer_frame (2026-04-29)
Closes the inert-defense pattern around the documented frame-
inference toggle on SepLogicConfig. The flag had no readers;
SepLogicEncoder::infer_frame always ran the full algorithm
regardless. Callers that only need entailment validity (without
the residual-frame computation) couldn't opt out for the
~30% reduction in encoder work on large heaps.
infer_frame now returns a typed FrameInferenceResult::failure
up front when the flag is false, with a diagnostic that names
the flag so callers can opt back in explicitly.
Four new pin tests cover (a) the documented default, (b) the runs-when-enabled path, (c) the skipped-when-disabled path with diagnostic-name assertion, and (d) the full default config round-trip.
Fixed — TacticConfig.allow_admits gates the admit / sorry tactics (2026-04-29)
Closes the inert-defense pattern around the allow_admits
flag (default true) on TacticConfig. The flag was
documented as "Allow admit/sorry tactics" but no code path
consulted it: even a verification run that explicitly opted
out (allow_admits = false) would still accept admitted
goals as proven.
The apply_admit and apply_sorry tactic handlers now
reject up front when the flag is false, with a typed error
that names the flag for diagnostic clarity. State is left
untouched so the goal stays open.
A public set_config / config() setter pair was added on
TacticEvaluator so callers (and tests) can change the
policy after construction without rebuilding the evaluator
state.
Six new pin tests cover the documented default, the
default-config no-fire path for both tactics, the gate-fires
path under allow_admits = false, and the setter round-trip.
This is the configuration that production / CI pipelines should run under: an admitted goal is a hole, not a proof.
Fixed — StaticVerificationConfig.memory_limit_mb reaches the SMT backend (2026-04-29)
Closes the inert-defense pattern around the documented 4 GB
memory ceiling on StaticVerificationConfig. The field had no
readers; setting memory_limit_mb = Some(64) had identical
effect to Some(1_000_000) because the SMT backend was never told.
The verifier's verify_with_timeout path now forwards the
value via smt-backend::set_global_param("memory_max_size", ...) before
opening a fresh the SMT backend context. Empirically this is the correct
scope: setting memory_max_size on the per-solver Params or
on the Config causes the SMT backend to silently mis-route queries (the
key is unknown at those scopes), but at the global-param scope
the limit takes effect.
None means "no caller-imposed limit" — the SMT backend uses its native
default. Subsequent calls overwrite the global value, so the
most-recent verifier configuration wins.
Five new pin tests cover the documented default, the None
opt-out, the boundary-value extremes, and the construction
contract.
Fixed — InterpolationConfig projection bounds reach the MBI engine (2026-04-29)
Closes the inert-defense pattern around two
InterpolationConfig fields that had documented defaults but
no readers:
max_projection_vars(default 100) — model-based interpolation projects the input formula onto shared variables via the SMT backend's quantifier-elimination tactic. The cost is exponential in the number of eliminated variables for some theories. The field now gatesproject_onto_shared: if the elimination set exceeds the budget, the engine returns a typed error before invoking the QE tactic.quantifier_elimination(defaulttrue) — when set tofalse,project_onto_sharedskips the QE step and returns the original formula. Interpolation correctness is preserved on the McMillan-styleA ⇒ Ihalf but the precision of theI ∧ B ⇒ ⊥half degrades; callers that prefer this trade-off for solver tractability can now opt out.
Five new pin tests cover (a) the documented defaults, (b) the extreme-budget construction paths, and (c) the QE-disabled construction path.
Fixed — ValidationConfig.check_well_founded rejects vacuous induction (2026-04-29)
Closes the inert-defense pattern around the check_well_founded
flag (default true) on ValidationConfig. The field was
documented as "Check that induction is well-founded" but no
code path consulted it, so the documented soundness contract
was a no-op.
The validate_induction path now rejects vacuous induction up
front: if substituting the induction variable with a probe
value leaves the property template structurally unchanged, the
variable is unused and the IH P(n) is syntactically identical
to the step obligation P(n+1) — the IH discharges the step
trivially regardless of whether P actually holds. The
emitted InductionError names the unused variable.
When the flag is false, the gate is bypassed and the legacy
induction logic runs unchanged (useful for callers that need
the older permissive behaviour for non-Nat-induction
experiments).
Three new pin tests exercise (a) the rejection path under the default config, (b) the bypass path under the flag, and (c) the default value itself.
Fixed — QEConfig.simplify_level controls the simplification tactic chain (2026-04-29)
Closes the inert-defense pattern around the documented 0-3
simplification level on QEConfig. The
QuantifierEliminator::new constructor previously hardcoded
Tactic::new("simplify") regardless of the configured
simplify_level, so the field had no effect on actual
simplification behaviour.
The level now maps to escalating the SMT backend tactic chains:
0→skip(identity tactic — no rewriting; chosen because composing withand_thenlater still works uniformly).1→simplifyonly.2(default) →simplifychained withpropagate-values.3and above →simplify+propagate-values+ctx-simplify(context-sensitive, more expensive).
Implemented via a private build_simplify_tactic(u8) helper
called by with_config; new() delegates to with_config
with the default config so both code paths honour the level.
Five new pin tests cover constructor success at each level plus the saturate-to-max behaviour for out-of-range values.
Fixed — RefinementConfig.timeout_ms now reaches the the SMT backend solver (2026-04-29)
Closes the inert-defense pattern around the public
RefinementConfig.timeout_ms knob (default 100 ms per spec).
Previously the value was held by SubsumptionChecker at
construction and never updated; the documented per-query
timeout had no effect on the underlying the SMT backend solver.
The wiring is now end-to-end:
SmtBackendgains aset_timeout_ms(&mut self, ms: u64)trait method with a no-op default so legacy backends compile unchanged.RefinementChecker::check_with_smtandverify_refinement_with_assumptionscallbackend.set_timeout_ms(self.config.timeout_ms)before every query.RefinementSmtBackend::set_timeout_msoverrides the trait method and forwards to its innerSubsumptionChecker::set_smt_timeout_ms.SubsumptionChecker::check_smtconfigures the the SMT backend solver'stimeoutparameter viaParams::set_u32on every fresh solver instance, mirroring the existing pattern inQESolver::fresh_solverand friends.
The documented "100 ms default per spec" now actually constrains
solver work; the SMT backend returns Unknown cleanly on timeout instead of
running unbounded against the host's wall clock. Five new pin
tests cover trait-default no-op, override observability, and
end-to-end timeout propagation through the bare checker.
Fixed — function-descriptor + constant + source-map memory-amp bounds (2026-04-29)
Final pass of the descriptor-level memory-amp campaign — closes
the last remaining unbounded varint-driven Vec / SmallVec
allocations in the VBC deserializer:
- Function descriptors —
type_params_count(≤ 64),params_count(≤ 256),ctx_count(≤ 32). Constant::Array— element count bounded atMAX_CONSTANT_ARRAY_LEN = 1 048 576.- Specialization entries —
type_args_countbounded atMAX_SPECIALIZATION_TYPE_ARGS = 64(matches the generic-fn type-param cap). - Source map —
files_countbounded atMAX_SOURCE_MAP_FILES = 65 536;entries_countbounded atMAX_SOURCE_MAP_ENTRIES = 4 194 304(4 M, comfortably above any real-module instruction-line count).
Every count consumed by Vec::with_capacity in the deserializer
is now bounded — a hostile .vbc artifact has zero paths to
reach with_capacity(usize::MAX) anywhere in the trust boundary.
Fixed — inner-descriptor memory-amp bounds (2026-04-29)
Continues the descriptor-level memory-amp campaign at the descriptor-recursion layer. The outer descriptor counts (type_params / fields / variants / protocols / methods) were bounded earlier; these are the counts the same descriptors recurse through:
MAX_BOUNDS_PER_TYPE_PARAM = 64— protocol bounds on a type parameter (fn f<T: P + Q>).MAX_FIELDS_PER_VARIANT = 1 024— per-variant struct fields (Some { a, b, c }).MAX_TYPE_REF_INSTANTIATION_ARGS = 64— generic instantiation arity (List<Int, String, …>).MAX_FN_TYPE_REF_PARAMS = 256— function-type signature parameter count.MAX_FN_TYPE_REF_CONTEXTS = 32— function-type context list (using [Logger, Database, …]).
These were the last unbounded varint-driven Vec / SmallVec
allocations in the VBC deserializer trust boundary. A hostile
descriptor recursion can no longer reach
with_capacity(usize::MAX) through any of these paths.
Fixed — descriptor-level memory-amp + parse_bytecode underflow (2026-04-29)
Closes the third memory-amplification class in VBC module deserialization, this time at the per-descriptor layer (inside type / function descriptors, not at the module-table level above).
Type / function / specialization descriptors carry varint-encoded
counts (type_params_count, fields_count, variants_count,
protocols_count, methods_count, …) that drive
SmallVec::with_capacity / Vec::with_capacity allocations.
Post the varint-canonicality fix below the largest accepted
varint is u64::MAX, which casts to usize::MAX on 64-bit —
most Rust allocators abort on with_capacity(usize::MAX). Tight
new bounds (per real-world descriptor surface):
MAX_TYPE_PARAMS_PER_DESCRIPTOR = 64(matches theast_to_typerecursion cap that already gates the front-end)MAX_FIELDS_PER_DESCRIPTOR = 4 096MAX_VARIANTS_PER_DESCRIPTOR = 4 096MAX_PROTOCOLS_PER_DESCRIPTOR = 256MAX_METHODS_PER_PROTOCOL_IMPL = 4 096MAX_DECOMPRESSED_BYTECODE_BYTES = 1 GB
The decompressed-size bound also closes a previously-trusted
allocation in the bytecode-section reader: a hostile compressed
section claiming uncompressed_size = u32::MAX would have made
the decompressor Vec::with_capacity ~4 GB before reading a
byte from the compressed stream.
Plus a real arithmetic-underflow fix: parse_bytecode's
None-compression branch computed section_size as usize - 1
to subtract the algorithm byte. For section_size == 0 this
underflowed silently in release builds (wrapping to
usize::MAX). The reader now rejects zero-size sections at
entry; subtraction afterwards is safe by precondition. The
bytecode-section reader's offset arithmetic also moved to
usize::checked_add for portable overflow defense.
Fixed — module-table memory-amp defense in VBC deserializer (2026-04-29)
Companion fix to the archive memory-amplification bounds below.
The per-module deserializer (verum_vbc::deserialize) had the
same class of bug: four header fields — type_table_count,
function_table_count, constant_pool_count, and
specialization_table_count — are u32 attacker-controlled
values, each used to drive a Vec::with_capacity(count as usize)
allocation before the deserializer reads a single entry. A
64-byte hostile module header could request 500 GB-2 TB of
allocations across the four tables before the file is even
consulted past its header.
Four architectural upper bounds enforced before any allocation:
MAX_TYPE_TABLE_ENTRIES = 1 048 576MAX_FUNCTION_TABLE_ENTRIES = 1 048 576MAX_CONSTANT_POOL_ENTRIES = 1 048 576MAX_SPECIALIZATION_TABLE_ENTRIES = 1 048 576
Real-world Verum modules carry at most a few thousand entries
in any of these tables; 1 M is comfortably above any plausible
module while staying far below the wraparound cliff. A new
typed TableTooLarge { field, count, max } error variant names
the offending field for immediate triage.
Closing the memory-amp class at the per-module boundary too means a hostile module loaded directly (not via an archive) can no longer amplify memory either.
Fixed — memory-amplification defense in VBC archive deserializer (2026-04-29)
read_archive in verum_vbc::archive previously trusted four
attacker-controlled size fields from the archive header for
allocation: module_count (u32), name_len (u32 per index
entry), dep_count (u32 per entry), and data_size (u64 per
module). A 32-byte hostile archive header could request
terabytes of allocations before the deserializer discovered the
file was too short — a memory-amplification denial-of-service.
Four architectural upper bounds are now enforced before any allocation:
MAX_MODULES_PER_ARCHIVE = 65 536MAX_MODULE_NAME_BYTES = 16 KBMAX_DEPS_PER_MODULE = 4 096MAX_MODULE_DATA_BYTES = 1 GB
Each rejection error message names the offending field so triage
is immediate. These bounds reflect "no real-world Verum archive
shipped through cog publish ever approaches this" — any input
that exceeds them is rejected as malformed before any allocation.
Fixed — usize-overflow path in length-prefixed decoders (2026-04-29)
decode_string and decode_bytes in the VBC encoding layer used
unchecked *offset + len arithmetic for the bounds check. With a
hostile varint length near usize::MAX and *offset > 0, the
addition wraps in release builds and the wrapped value passes the
> data.len() check, opening a path to read from the wrong
region. Both decoders now use usize::checked_add and surface
overflow as Eof. Companion fix to the byte[9]-canonicality
defense below — together they close the two known integer-class
defenses at the bytecode-decoder layer.
Fixed — varint canonicality at the bytecode trust boundary (2026-04-29)
Tightens the decode_varint / read_varint decoders in
verum_vbc::encoding to reject adversarial 10-byte encodings whose
final byte sets bits 1..6. At shift = 63 only bit 0 of byte[9] is
representable in u64; the previous decoders silently dropped the
upper bits via the platform's shift-out-of-range semantics, so 64
distinct invalid inputs collapsed onto u64::MAX. Both decoder
surfaces now return VarIntOverflow for any such encoding. The
legitimate boundary u64::MAX (byte[9] == 0x01) is still accepted.
Mirrors the protobuf read_varint Google-reference behaviour
already enforced in core/protobuf/wire.vr.
Fixed — hostile-size allocation in interpreter dispatch (2026-04-29)
VBC interpreter dispatch handlers (CbgrAlloc in
ffi_extended.rs; GpuAlloc, MallocManaged, GpuMemAlloc,
Free in gpu.rs) used to either panic via .unwrap() on a
chained Layout::from_size_align fallback, or silently downgrade
to a 1-byte layout via unwrap_or(Layout::new::<u8>()) while the
caller still believed they got size bytes (heap overflow on the
first write past byte 0; UB on the matching dealloc since
std::alloc::dealloc with a wrong layout is undefined behaviour).
Allocation paths now return a null pointer on layout failure
(standard malloc-fail contract); the deallocation path leaks on
layout failure rather than dealloc with a wrong layout.
Added — UTF-8-safe text primitives in verum_common (2026-04-29)
verum_common::text_utf8 consolidates six ad-hoc UTF-8 routines
into one canonical module: clamp_to_char_boundary, safe_prefix,
truncate_chars, find_word_bounds, char_before_satisfies,
char_at_satisfies. All zero-allocation, stdlib-only
(is_char_boundary / char_indices), O(prefix-length). LSP
(completion, rename, quick_fixes, diagnostics,
document::word_at_position, script::incremental) and VBC
(disassemble) now delegate to the shared module — eliminates the
byte-vs-char-index bug class that had produced 13 panic / silent-
corruption sites across 8 distinct files.
Added — VBC module-load trust boundary (2026-04-28)
A two-tier loader API with explicit trust contracts replaces the implicit "everything is trusted" assumption that previously gated production module loads. Closes round-1 §3.1 (hand-crafted bytecode violating type-table invariants), round-2 §3.1 (assign to read-only register), and round-2 §3.2 (mismatched arity calls) of the red-team review.
New strict entry points in <implementation>:
deserialize::deserialize_module_validated(data)— structural decode → content-hash verification → dependency-hash verification → per-instruction bytecode validation.archive::VbcArchive::load_module_validated(name)— same, applied to archive entries (handles decompression).interpreter::Interpreter::try_new_validated(module)— runs the validator over a pre-loadedArc<VbcModule>before construction.
The lenient deserialize_module / load_module / try_new entry
points are preserved for in-process-emitted bytecode where the
validator's O(N) walk is wasted work.
What the validator catches at load time (instead of execution time / silent corruption):
- Out-of-range
FunctionId/ConstId/StringId/TypeIdcross-references. - Register references past the function's declared
register_count. - Branch offsets falling outside the function's bytecode region OR landing mid-instruction in another instruction's operand stream (Jmp / JmpIf / JmpNot / JmpCmp / Switch / TryBegin).
- Call-arity mismatches: every
Call/TailCall/CallGhasargs.countchecked against the target function's declaredparams.len(). - Decoder failures mid-stream.
- Content-hash tampering: blake3 over
data[HEADER_SIZE..]recomputed and matched against the header'scontent_hash. - Dependency-hash tampering: the cog-distribution dependency graph's u64 fingerprint.
A new InterpreterError::ValidationFailed { module_name, reason }
variant carries forensic detail. The aggregate
VbcError::MultipleErrors(Vec<VbcError>) now renders with a
header line followed by indented numbered per-error entries,
exposing the full defect list to the user instead of a count-only
summary.
See VBC Bytecode → Module-load trust boundary.
Added — Opcode::Extended general-purpose extension byte (2026-04-28)
Reserved opcode 0x1F (formerly the unused IntArith1F slot) is
now Opcode::Extended. Wire format [0x1F] [sub_op:u8] [operands...]. Foundation for #146 Phase 3 (MakeVariantTyped);
sub-op 0x00 is reserved as a forward-compat anchor that decoders
must accept and skip without breaking older interpreters.
Added — extraction lowerers + @extract(realize=) directive (2026-04-27)
verum extract AST-lowerer expansion:
- Match expressions, MethodCall, Field access, Closures (no contexts /
no async / no move), Pipeline (
|>), Tuples + TupleIndex, Index, and NullCoalesce now flow through the OCaml / Lean / Coq partial-coverage lowerers. Each construct is emitted in idiomatic per-target syntax with gracefulNonefallback when a sub-shape exits the lowerer's vocabulary.
@extract(realize="<fn_name>") directive:
- Short-circuits the body-synthesis path. The verified surface
signature is preserved; the emitted body is a thin wrapper that
delegates to the named native function. Extends
@extract,@extract_witness, and@extract_contractwith the samerealize=keyword. Lets a verified specification bind to a hand-written / runtime-intrinsic primitive (crypto stub, intrinsic wrapper, foreign syscall) without losing proof-checked types at the boundary.
See Verification → Program extraction.
Added — linter production hardening + stdlib algebra surfaces (2026-04-26)
Linter (verum lint) — promoted to 100 % production-ready.
- Lex-mask: every text-scan rule now consults a per-byte
classification (Code / LineComment / BlockComment / String /
RawString) so substrings inside string literals or comments no
longer fire
deprecated-syntax,todo-in-code,unbounded-channel, etc. Multi-byte UTF-8 (em-dash, CJK, math symbols) handled correctly — earlier the masked-view builder produced invalid UTF-8 on multi-byte chars in comments. - Unified parse: per-file phase hands its parsed
Moduleto the cross-file phase vialint_one_with_cache → CorpusFile, eliminating the second parse per file. Cache-hit entries are re-parsed in a single batched pass. parse-errormeta-rule: parser failures surface as a structured diagnostic (Error / Safety) so users see when AST passes were skipped. Always on; cannot be suppressed.- Structured
--fix:apply_fix_edits(content, &[FixEdit])is the canonical edit applier (LSP-style 1-indexed ranges, reverse-order application, overlap detection).synthesize_fix_edits_for(issue, content)covers all 9 fixable rules; on-disk--fixand JSONfix.editsconsumers produce byte-identical output. Old per-rule line-rewrite helpers retired. - Streaming JSON:
--format json(without--baseline/--fix) flushes each file's diagnostics as soon as that file's per-file phase completes — time-to-first-byte drops from corpus-latency to single-file-latency. Order is non-deterministic; schema-stable identity is(rule, file, line, column). - New CLI flags:
--watch/--watch-clear,--threads,--no-cache/--clean-cache,--baseline FILE/--write-baseline/--no-baseline,--max-warnings N,--new-only-since GIT_REF,--list-groups.
Tests: 47 → 173+ lint tests across 19 test files.
Stdlib algebra surfaces — modules that previously shipped only data-type definitions now ship the full algebra promised by their doc-strings.
core.eval.cbpv—cbpv_occurs_free, capture-avoidingcbpv_substitute,CbpvStepoutcome type,cbpv_step(β / force-thunk / sequence-bind with congruences),cbpv_normalise(t, gas)to fixed point,cbpv_alpha_eq.core.control.continuation—CcStep,cc_step(β / reset-value / shift-capture),cc_normalise(t, gas),cc_alpha_eq.core.logic.linear—lin_to_nnf(de Morgan + involutivity),lin_negate,lin_is_nnf,lin_eq,lin_size,lin_atom_count.core.logic.kripke—valid_in_frame,semantically_equivalent, frame-property predicatesis_serial/is_reflexive/is_transitive/is_symmetric/is_euclidean(modal axioms D / T / 4 / B / 5),is_s5.core.types.poly_kinds— full Robinsonkind_unify+kind_apply+kind_compose, plusis_concrete,kind_arity,apply_args,free_vars.core.types.qtt—mul_quantity(multiplicative scaling under λ-binders),is_sub(subquantity latticeZero ≤ One ≤ AtMost(n) ≤ Many),top_quantity/bottom_quantity,quantity_eq.core.meta.tactic— recursivemeta_normalise(bottom-up β-cancel + seq-elim),meta_is_normal,seq_eliminate.
Intrinsic safety contracts —
core/intrinsics/arithmetic.vr div / rem / neg / abs / mul /
wrapping_div / wrapping_rem now document panic conditions
(b == 0, T::MIN / -1, T::MIN for neg / abs, IEEE 754 float
behaviour) per the convention set by
core/intrinsics/memory.vr.
Added — wide stdlib primitive expansion (2026-04-22/23)
A large batch of reusable user-level primitives shipped across
encoding / security / collections / base / time /
metrics / async / net. Every addition carries a
typecheck-verified VCS test under vcs/specs/L2-standard/.
Encoding — base32 (RFC 4648 §6), base58 + base58check
(Bitcoin), cbor (RFC 8949 with canonical map sort + f16/f32/f64
widening), msgpack, jcs (RFC 8785 UTF-16 code-unit sort), pem
(RFC 7468 label-agnostic), json_pointer (RFC 6901).
Security — hpke (RFC 9180 Mode Base, DHKEM-X25519 +
ChaCha20-Poly1305), jwt (RFC 7519/7515 with alg:none rejected +
algorithm-confusion blocked), cose (RFC 9052 Sign1 + Mac0), otp
(HOTP/TOTP RFC 4226/6238), password_hash + pbkdf2 (PHC modular
format, 100k-iteration floor), merkle (RFC 6962, CVE-2012-2459-safe
odd-leaf promotion), token (CSPRNG session/CSRF/OTP),
server_identity (RFC 6125), hash/crc32c, hash/xxhash (XXH64),
hash/murmur3 (32 + 128-bit Cassandra-compatible).
Collections — lru, ttl_cache, bloom, hyperloglog,
count_min, reservoir (Vitter Algorithm R), consistent_hash
(Ketama-compatible).
Base — snowflake, nanoid, semver, glob
(fnmatch(FNM_PATHNAME|FNM_LEADING_DIR) semantics).
Time — rfc3339 (ISO 8601 w/ Howard Hinnant date math),
cron (POSIX 5-field with Vixie OR-semantics).
Metrics — ewma (fixed-α + Dropwizard time-decaying +
RateMeter with 1/5/15-minute windows).
Async — semaphore (cooperative task limiter, RAII permit),
backoff (exponential / decorrelated / Fibonacci with jitter).
Net — content_negotiation (Accept / Accept-Encoding /
Accept-Language q-factor selection), http_range (RFC 9110 §14),
link_header (RFC 8288), proxy/rate_limit (TokenBucket +
LeakyBucket + SlidingWindow under one RateLimiter protocol).
QUIC / TLS (warp stack) — stateless_reset (RFC 9000 §10.3),
cid_pool + CidIssuer, key_update (RFC 9001 §6 + §6.6),
address_token (§8.1.3), pacer (RFC 9002 §7.7), stats +
stats_prometheus, batch_io (GSO/GRO/sendmmsg); TLS 1.3
sni_resolver (RFC 6066), zero_rtt_antireplay (RFC 8446 §8
with ReplayGuard protocol), resume_verify, client_session_from_nst,
ticket_issuer. HTTP/3: h3/priority (RFC 9218).
Fixed — Heap<dyn P>.method() / Shared<dyn P>.method() dispatch
Smart-pointer receivers carrying a dyn-protocol payload now resolve protocol methods through the auto-deref cascade end-to-end.
Previously h.start_span(...) on h: Heap<dyn Tracer> failed with
"no method named start_span found for type &dyn Tracer" — the
cascade correctly unwrapped Heap<dyn P> to &dyn P (DynProtocol is
unsized and must live behind a reference), but the early DynProtocol
resolution branch ran before the cascade, so cascade-derived
&dyn P receivers were never matched.
The fix adds a post-cascade DynProtocol resolution that peels one
reference layer and serves the method from
protocol_checker.get_method_type(bound, method). Combined with the
cascade, the full chain Heap<dyn P> → &dyn P → peel → dyn P → protocol method now succeeds. type_or_dyn_has_method also peels one
reference layer so the cascade's halt condition agrees with the
resolver. No hardcoded smart-pointer list — the stdlib's
Deref::Target associated-type declarations drive the cascade, and
the DynProtocol's own bounds drive the resolution.
Regression test:
vcs/specs/L1-core/types/dynamic/heap_dyn_dispatch.vr covers
Heap<dyn P>, Shared<dyn P>, and direct &dyn P receivers.
See Architecture — Smart-pointer receivers calling protocol methods.
Fixed — impl-level type parameter positional alignment
implement<I: Iterator, B, F: fn(I.Item) -> B> Iterator for MappedIter<I, F>
no longer poisons B when .next() is invoked. The previous
declaration-order scheme.vars ([I, B, F]) combined with
bind_limit = 2 (matching for_type = MappedIter<I, F>'s two slots)
bound B to the closure type, surfacing as
Maybe<fn(Int) -> Int> instead of Maybe<Int> at .next() call
sites — with misleading "expected Int, found fn(Int) -> Int" errors.
The fix partitions impl-level TypeVars by whether they appear in
for_type.free_vars() and reorders them as three blocks:
- Impl vars in
for_type, in declaration order (positional binding slots,impl_var_count = block size); - Impl vars outside
for_type(left free, inferred from bounds or unification at the call site); - Method-level TypeVars.
Now bind_limit = impl_var_count aligns perfectly with
receiver.args.len(). Applied in both the inherent and protocol
branches of register_impl_block_inner.
Regression test:
vcs/specs/L2-standard/iterator/impl_param_reorder.vr — the
once_with(|| 5).map(|x| x*10).next() reproducer.
See Architecture — Positional-alignment reordering.
Added — reference-grade tactic DSL
Industrial-grade extensions to the proof-engine surface. The tactic language now matches the expressive power expected of a modern proof assistant (Coq/Lean tier), while remaining a natural extension of ordinary Verum syntax.
- Block-form combinators.
first { t₁; t₂; t₃ }— the block form specified in grammar §2.19.7 — now parses alongside the list formfirst [t₁, t₂, t₃].repeat,try/try … else,all_goals, andfocusalready accepted block bodies;firstnow does too. Enablesring_law,field_law,category_lawand the fullcore/math/tactics.vrstrategy library (previously 284 parse errors, now 0). - Generic tactics.
tactic category_law<C>() { … }declares a polymorphic tactic; call sites pass explicit type arguments:category_law<F.Source>(). An optionalwhereclause supports protocol bounds. - Typed parameters with defaults. Tactic parameters accept both the
classical kinds (
Expr,Type,Tactic,Hypothesis,Int) and two new forms:Prop(first-class propositions) and arbitrary type expressions (Float,List<T>,Maybe<Proof>, …). Default values are declared with= expr, e.g.oracle(goal: Prop, confidence: Float = 0.9). - Structured tactic bodies. Tactics can bind local state, branch on
values, and fail with diagnostics:
let x: T = expr;— monadic let-binding inside the tactic body;match scrutinee { P => tactic, … }— pattern-directed branching;if cond { t₁ } else { t₂ }— conditional tactic execution;fail("reason")— explicit failure feeding into enclosingtry/firstcombinators.
- Reserved-keyword tactic names. Users can declare tactics named
after built-ins (
tactic assumption() { … },tactic contradiction() { … },tactic ring() { … }, etc.) — the declaration shadows the built-in within its module.
The parser, AST, visitor, proof-checker, tactic evaluator, and quote
backend were updated end-to-end. New anchors in
vcs/specs/L1-core/proof/tactics/ lock the grammar. The stdlib
parse-success count moved from 2/10 → 6/10 math modules
(cubical, day_convolution, infinity_topos,
kan_extension, tactics, theory_interop.core all parse
cleanly now).
See Proof DSL — tactic declarations
and reference/tactics — User-defined tactics.
Added — crash reporter and verum diagnose
- New
verum_error::crashmodule installs a process-wide crash reporter atverumstartup. It captures every panic and every fatal signal (SIGSEGV,SIGBUS,SIGILL,SIGFPE,SIGABRTon Unix viasigaction+sigaltstack;SetUnhandledExceptionFilteron Windows) into a paired.log(human) +.json(schema v1) report under~/.verum/crashes/. Reports include the exact command and cwd, a filtered environment with secret-looking keys redacted, the build identity (verumversion, git SHA, profile, target,rustc --version), the thread name, a Rust backtrace, and a breadcrumb trail. - New
verum_error::breadcrumbmodule — a thread-local RAII trail mirrored to a cross-thread snapshot so the signal handler can include the last-known phase even when the offending thread's TLS is unreachable. The compilation pipeline emits breadcrumbs atstdlib_loading,project_modules,load_source,parse,type_check,verify,cbgr_analysis,ffi_validation,rayon_fence,generate_native,codegen.vbc_to_llvm, andinterpret. - New
verum diagnosesubcommand family:list [--limit N]— index of recent reports with one-line summaries (kind, message, build, last known phase);show [REPORT] [--json] [--scrub-paths]— full report to stdout, optionally path-scrubbed for external sharing;bundle [-o OUT] [--recent N] [--scrub-paths]—.tar.gzsuitable for attaching to an issue; a README inside the archive explains where to upload it;submit [--repo owner/name] [--recent N] [--dry-run]— opens a new GitHub issue viaghCLI with the latest report summary pre-filled (paths scrubbed);env [--json]— print the captured build/host snapshot;clean [--yes]— wipe the report directory.
- New
[profile.release-debug-tables]in the workspaceCargo.toml— inherits fromreleasebut keepsdebug = "line-tables-only"+split-debuginfo = "packed"so crash-report backtraces resolve tofile:line. The main binary size is unchanged; line tables live in an external.dSYM/.dwpbundle.
Fixed — non-deterministic SIGSEGV in AOT codegen
Release builds on arm64 macOS SIGSEGV'd in ~60–70 % of
verum build ./examples/cbgr_demo.vr invocations, always on the
main thread, always inside LLVM pass-constructor initialisation
(TargetLibraryInfoWrapperPass, CFIFixup, CallBase,
MachineDominatorTreeWrapperPass, GCModuleInfo — all under
__cxa_guard_acquire → __os_semaphore_wait). Diagnosed via the
new crash reporter: 14/14 reports pointed at
compiler.phase.generate_native at 307–350 ms into the phase.
Two surgical fixes:
- Eager native-target init —
verum_cli::mainnow callsTarget::initialize_nativeas its first line, before the stdlib parse can spawn rayon workers or the verifier can touch the SMT backend. The IR-level pass registry is fully populated on the main thread while no other thread is alive, releasing the cxa guards before the fault window. - Real rayon fence before LLVM —
rayon::yield_now()inphase_generate_nativeis replaced withrayon::broadcast(|_| ()), which dispatches a no-op task to every worker and waits for completion. Parked workers wake, run, and re-park before LLVM touches its remaining cxa guards, eliminating the wake-path vs lazy-init race.
100-run stress test: 0 / 100 crashes after the fix. Guarded by
tier1_repeated_aot_build_is_stable in
a cli integration test.
Fixed — duplicate "Running" line in single-file run
verum run file.vr printed Running <file> (interpreter) twice —
once from main.rs and once from the single-file tier dispatcher.
The dispatcher's duplicate line is gone.
Docs
- New Tooling → Crash diagnostics page
covering the crash reporter, breadcrumbs, report layout, the
verum diagnosecommands, signal-safety caveats, and therelease-debug-tablesprofile. - Reference → CLI commands now
documents the
verum diagnosefamily. - Guides → Troubleshooting has a new "Compiler crashes" section that walks the list → show → bundle → submit flow.
[0.1.0] — 2026-04-17 — runtime foundations, first public version
Fixed — VBC + AOT byte-slice semantics
Text.as_bytes()andslice_from_raw_parts(ptr, len)now produce proper slice values in both tiers. The VBC lowering previously emittedPack(a heap tuple) for slices, so.len()returned 2 (the tuple arity),bytes[i]returned an 8-byte NaN-boxedValueinstead of a byte, and every CBGR slice op silently fell through itsis_fat_ref()guard. NewCbgrSubOpcode::RefSliceRaw(0x0A) builds aFatRefdirectly from (ptr, len) withelem_size=1. NewTextSubOpcode::AsBytes(0x34) materialises a byte-sliceFatReffrom either the NaN-boxed small-string representation or the heap-allocated[header][len:u64][bytes…]layout; the codegen intercepts.as_bytes()onTextreceivers and routes through this op soself.ptrviaGetF(which reads the wrong offset for both representations) is never called at runtime.- Matching AOT/LLVM handlers lower
TextExtended::AsBytes(reads the pointer viaverum_text_get_ptr, readslenfrom field 1 of the flat{ptr, len, cap}struct) andCbgrSubOpcode::RefSliceRawinto the standard 40-byte Pack-object slice layout, so the AOTLen/GetE/IterNewhandlers already in place pick up the fix without further change. CbgrSubOpcode::SliceGet,SliceGetUnchecked, andSliceSubslicenow honourfat_ref.reservedas the element stride (1/2/4/8 for raw integer arrays, 0 for NaN-boxed Values). Previously they hard-codedsizeof(Value)and walked 8 bytes per index, so indexing or subslicing a byte slice produced garbage.
Fixed — variant-tag consistency
Maybe<T>is declaredNone | Some(T), soregister_type_constructorsassignsNone=0, Some=1positionally. The hard-coded fallback table incodegen/mod.rshadSome=0, None=1— the stdlib- constants pass ran first andregister_functionoverwrote by arity, soNoneended up tagged as1while pattern matching (which derives tags from declaration order) expected0. EveryNonevalue silently matchedSome(x) =>arms and boundxto uninitialised payload memory. Tags are now consistent across all three sites (register_stdlib_constants, builtin registration aroundcompile_program,register_type_constructors) and the runtime helpers (make_maybe_int,make_some_value,make_none_value).- Bare
Noneidentifiers were lowered through the__const_path toLoadI 0instead ofMakeVariant tag=0. Zero-arity variant constructors now route throughMakeVariantbefore the constant path. TextExtended::AsByteshandler auto-derefs both CBGR register references andThinRefbefore inspecting the Text layout, sos.as_bytes()inside a function takings: &Textno longer returns an empty slice.
Fixed — slice method dispatch
implement<T> [T]blocks register under theSlice.*prefix (becauseextract_impl_type_name_from_typemapsTypeKind::Slice(_)to"Slice"), but the method-dispatch codegen was formatting[Byte].methodas the lookup key (usingextract_type_name_from_ast). The two halves of the pipeline disagreed; method names now get normalised so[…].method→Slice.methodbefore interning.core.collections.slicewas present in the AOT-retention list but not in the primaryALWAYS_INCLUDEthat controls which stdlib modules get compiled into the VBC module at all. Added, so the normalisedSlice.*lookups actually have bodies to find.- Intercept
[T].slice(start, end)at codegen and emitCbgrSubOpcode::SliceSubslicedirectly, bypassing the compiled stdlib body (which panics insideValue::as_i64when it receives aFatRefreceiver viaCallM). extract_type_namenow handlesTypeKind::Slice(T) → "[T]"instead of returningNone, so method-chain inference carries slice-ness through calls likes.as_bytes()and downstream.slice()/.get()/.is_empty()dispatch can route correctly.
Fixed — stdlib parsing and Text layout
Text.parse_int/Text.parse_floatroute through the pure-Verumparse_int_radix(10)path instead of the legacytext_parse_int/text_parse_floatintrinsics, whose runtime declarations returnedMaybe<T>while the stdlib wrappers were typed asResult<T, ParseError>. BecauseMaybe::Some(n)has tag 1 andResult::Err(e)also has tag 1, every successful parse was read back asErr(n)—"42".parse_int()returnedErr. The new implementation parses bytes directly with explicit error messages (empty string,no digits,invalid character,digit out of range,trailing characters,missing exponent digits).Textvalues have two coexisting runtime layouts under the sameTypeId::TEXT:- static / intrinsic-built heap strings:
[header][len:u64][bytes…] - stdlib builder
Text {ptr, len, cap}: three NaN-boxedValuefields produced byText.new()+push_byte. Both theLenopcode handler and theTextExtended::AsByteslowering were decoding only the first case. For a freshText.new(),.len()read the null-pointer bit pattern as a u64 and reported9221120237041090560;.as_bytes()produced a FatRef into random memory. Both now disambiguate by object size + field tags and report correct values for builder Text.
- static / intrinsic-built heap strings:
Impact
Pure-Verum byte-level stdlib code (JSON, base64, URL, UUID, hex
decoders; regex engines; TLS framing; binary protocols) can now
parse numeric tokens and traverse bytes end-to-end in the VBC
interpreter and AOT builds. Prior to these fixes every such module
typechecked cleanly but crashed or silently corrupted data at run
time; the VCS typecheck-pass suites did not surface it because
they never executed the code.
Concrete known-working cases:
"42".parse_int()→Ok(42),"-7".parse_int()→Ok(-7),"abc".parse_int()→Err("digit out of range")."hello".as_bytes().slice(1, 4)→ 3-byte slice at"ell".JSON.parse("42"),JSON.parse("true"),JSON.parse("\"hi\""),JSON.parse("[1, 2, 3]"),JSON.parse("{}")all returnOk.
Fixed — byte-sized writes via memset
The generic ptr_write<T> intrinsic lowered to DerefMut, which
writes 8 bytes of a NaN-boxed Value regardless of T. Writing a
Byte this way corrupted seven bytes past the target. All byte-
granularity writes in the stdlib are now expressed as
memset(ptr, byte, 1) with an explicit 1-byte length:
Text.push_byte— core of every Text builder path.Text.from_utf8_unchecked— null terminator after memcpy.Text.grow()— null-terminator maintenance when capacity expands.Text.make_ascii_lowercase/make_ascii_uppercase— in-place flips.
Also reconciled core/encoding/json.vr with the new
Text.parse_int / parse_float signatures (Result<T, ParseError>
after the parse-int fix). The JSON number parser was still pattern-
matching Some(i) / None; because Result::Ok and Maybe::None
share tag 0, every successful integer parse silently routed to the
"integer out of range" fallback. Now uses Ok / Err arms directly.
Known-working after this layer of fixes: Text.new(); t.push_byte(b);
round-trips; t.as_bytes() yields the written bytes.
Fixed — Text equality + hashing for builder layout
The interpreter's resolve_string_value, extract_string,
value_hash, and heap_string_content_eq all previously decoded
only the heap-string Text layout ([len:u64][bytes…]). For a
builder Text {Value(ptr), Value(len), Value(cap)} they read the
NaN-boxed ptr field as a u64 length, returned garbage strings
(breaking ==) and looped over an out-of-bounds byte count
(crashing Map.insert). All four helpers now disambiguate by
object size + field tags, matching the pattern used by
handle_array_len / TextExtended::AsBytes. A shared
text_value_bytes_and_len helper now owns the extraction.
Concrete effect:
built("a") == built("a")→true(wasfalse)built("a") == "a"and reverse →true(wasfalse)Map<Text, Int>.insert(built_key, 1)→ no crashJSON.parse("{\"a\":1}")→Ok(was SIGBUS)
Fixed — Map.iter() + for-loop tuple destructuring + to_text
map.iter()/set.iter()at the interpreter level now return the receiver unchanged.IterNewalready recognisesTypeId::MAP/TypeId::SETand builds the rightITER_TYPE_MAPiterator, so wrapping the map in a second iterator object (the first attempt) only confusedIterNewinto treating it as a list.IterNextforITER_TYPE_MAPnow yields(key, value)2-tuples (heap-allocatedTypeId::TUPLEobjects) matching the shape the codegen destructures forfor (k, v) in …. The same change applied to the method-level iterator dispatchnextarm as a defensive layer.is_custom_iterator_typenow recognises the stdlib iterator wrappers (MapIter,MapKeys,MapValues,SetIter,ListIter, …) as "builtin-like", sofor (k, v) in m.iter()goes through theIterNew/IterNextpath rather than dispatching to the uncompiledMapIter.has_next/.nextstdlib methods.dispatch_primitive_methodnow acceptsto_textas an alias forto_string—Text(notString) is Verum's native string type and the stdlib uses.to_text()throughout (e.g. JSON'si.to_text()for integer serialization).
Known-working: for (k, v) in m.iter() { … } iterates every entry
and destructures the tuple correctly; JSON stringify advances
through the object-write path without runtime errors. Serialization
still has a shallower bug (the integer value's bytes aren't being
appended to the output buffer), but the infrastructure — iteration,
method dispatch, primitive-to-text conversion — is in place.
Fixed — (0..N).collect() infers from let-binding context
let v: List<Int> = (0..10).collect(); errored with
"Type mismatch: expected 'Listinfer_method_call_inner_impl returned the element
type for .collect() on adapter-like receivers — Range<Int>
ended up as Int, which then couldn't unify with the let-binding's
List<Int> annotation.
Short-circuit the entire dispatch chain when the method is a
0-argument collect(): return a fresh type variable. Bidirectional
check_expr then unifies the var with whatever the let-binding
annotation supplies. With no annotation the call site is genuinely
ambiguous (which it should be); add an explicit : T if needed.
Removes 3 L0 reference_system/performance failures
(cache_effects.vr, memory_overhead.vr, reference_locality.vr).
Fixed — stabilize_ref_source was over-eager and broke CBGR safety
Follow-up to the &temp ref-source stabilization: the original
deny-list ("not a named local") was too permissive and promoted
&*heap_val into &fresh_copy_of_heap_val. The freshly allocated
stable slot has no link back to the CBGR-tracked allocation, so a
subsequent drop(heap_val); *r returned the snapshot instead of
panicking with "CBGR use-after-free detected". Six L0 specs that
exercise the panic path silently regressed (exited 0 instead of
panicking).
Switched to an allow-list: only stabilize for the shapes that
actually produce recyclable temps — Index, Field, TupleIndex,
Binary, Call, MethodCall. Deref is not in the list, so
&*heap_val keeps its Tier 0 ref-to-source-slot semantics and the
generation/epoch checks fire as designed.
L0 lexer/parser/types/builtin-syntax/memory-safety/mmio/modules/
reference_system: 577/587 = 98.3% (10 remaining failures are
all stdlib-API gaps — Register.modify missing and
Epoch.current() recursion).
Fixed — &temp references survive past the next alloc_temp
Taking a reference to a temporary value (&arr[i], &(a + b),
&f(), …) emitted a Tier 0 CBGR ref encoding the inner register's
absolute index. The interpreter's Deref then read back through
that index — but the temp pool would happily recycle the slot the
moment the next alloc_temp ran. The deref then read whatever
happened to land in the slot (an f-string text fragment, a
print-format intermediate, …):
let arr = [1, 2, 3, 4, 5]; let r: &Int = &arr[2]; print(f"*r = {*r}"); // → "*r = " (nothing — wrong) assert_eq(*r, 3); // → fails
compile_unary now stabilizes the source via
stabilize_ref_source: when the inner expression isn't a named
local, allocate a fresh, never-recycled register, copy the value
into it, and reference that. One extra Mov per &temp (the
common Tier 0 case is already paying the 15 ns generation check),
and the silent slot-collision class of bugs is closed at codegen
time. Applies to all six tier/mutability variants
(Ref/RefMut/RefChecked/RefCheckedMut/RefUnsafe/
RefUnsafeMut). Mutable element refs (&mut arr[i]) still don't
write back to the array storage — that needs a separate
"element write-through" opcode (tracked).
Cleared — remaining clippy warnings
Eight stylistic lints across verum_vbc, verum_smt,
verum_types, verum_mlir. None affect behavior:
verum_vbc: drop the1 *and+ 0no-ops in the CbgrAlloc Ok-wrap, redundantas *mut u8casts on already-*mut u8pointers, collapse a nestedif {…}inside amatches!-guarded layout-property branch, replaceis_some + unwrapwithlet Maybe::Some(ref body) = ….verum_smt: drop the redundant outer..Default::default()inSmtConfig::debugging.verum_types: invert theLayerPartialOrd/Ord pair socmpis the canonical implementation; replacemin(64).max(1)withclamp(1, 64)inBitVec::new.verum_mlir: add aDefaultimpl forLlvmContextRef.
cargo clippy --workspace --bins --lib is now clean (no warnings
outside upstream crates that build C/C++ via the system ar).
Fixed — .method() doesn't fall through to a free fn
nums.map(|n| n * 2) panicked at runtime with "method 'Map.resize'
not found on value". Two compounding bugs:
compile_method_call's static-receiver branch treated bare-Path receivers as type names even when the segment was a local variable. A regression from theFoo<T>.method(...)static dispatch unification — the localnumsgot lifted into a would-benums.mapstatic lookup.- The interpreter's
handle_call_methodresolved the resulting bare"map"string by suffix-scanning the entire registered function table, andcore/collections/map.vr's top-levelpub fn map<K,V>(pairs)happened to register beforeList.map. The dispatcher picked it, ran intoMap.with_capacity→self.resize(cap), and the privateMap.resizeis not in the function table.
Codegen now suppresses the static-receiver intercept for local
variables; the dispatcher restricts unqualified-suffix matches to
candidates that contain a . (i.e., methods, not free fns). After
the fix L0 lexer + parser + types + builtin-syntax runs at
323/323 = 100% (was 322/323). closure_runtime.vr —
xs.map(...), .filter(...), .fold(...), captured environments,
nested closures, higher-order functions — passes end-to-end.
Fixed — AOT .await on a direct async-fn call no longer SIGSEGVs
verum run --aot (and the resulting verum build binary) crashed on
the first .await of a plain async-fn call:
async fn add(a: Int, b: Int) -> Int { a + b } fn main() { let r = add(1, 2).await; print(f"{r}"); }
Async fns in the current implementation are not compiled to suspend
/resume state machines — add(1, 2) runs the body inline and returns
the value (3). The interpreter's Await handler tolerated that (it
pattern-matches on a sentinel-encoded task ID and falls through to
pass-through). The AOT lowering, however, called
verum_pool_await(handle_i64), which int_to_ptr'd the small int
and dereferenced it as a 16-byte pool handle struct.
Fix: compile_await no longer emits Instruction::Await when the
inner expression is anything other than ExprKind::Spawn. The result
of the async-fn call is the awaited value; no runtime poll is needed.
spawn { … }.await keeps the threaded path. Removes the "AOT Async
— No Polling Executor" entry from KNOWN_ISSUES.md.
Added — REPL VBC-backed evaluation
verum repl now actually evaluates each prompt instead of stopping at
parse + typecheck. Each input is classified and routed:
let NAME [: TYPE] = EXPR→ desugars tostatic NAME: TYPE = EXPR;(with type annotation) orconst NAME = EXPR;(without). The declaration is appended to a session source buffer that persists across prompts.- Top-level items (
fn,type,protocol,implement,static,const) → appended to the session source after a compile-only validation. - Bare expressions → wrapped in
fn __repl_main_<N>() { print(f"{...}"); }, the session source plus the wrapper is compiled to VBC, and the wrapper is executed viaverum_vbc::interpreter::Interpreter. The captured stdout is printed as the result.
:source shows the accumulated buffer, :reset clears it. Removes
the "REPL — Parse-Only" entry from KNOWN_ISSUES.md.
Fixed — Shared<T>::new lowering (closes the last KNOWN_ISSUES item)
Shared<Int>.new(42) (and any Foo<TypeArgs>.method(...) call on
a generic type) blew up at codegen with two latent bugs in the VBC
expression-codegen path:
- Field access on an
ExprKind::TypeExprhad no layout-property handler.SharedInner<T>.sizeandSharedInner<T>.alignmentfell through to a generic field load that returnedi64::MAX(the debug-formatted Type string interpreted as an integer). The stdlib then asked the allocator for ~9 EB and panicked with "Out of memory". Addtry_resolve_type_layout_property(TypeExpr) andlayout_property_for_named(bare-Path generic params and user structs). In VBC's NaN-boxed model, every record slot is exactly one 8-byteValue, so the answer isfield_count * 8for size, 8 for alignment, and the type arguments are layout-irrelevant. - Method dispatch on a
TypeExprreceiver fell throughtry_flatten_module_path(which only knowsPathnodes) and compiled the receiver as a runtime value — emittingLOAD_K String("Type { kind: Generic { …")followed bySetFagainst garbage intern indices liker1.306. Extract astatic_receiver_typehelper that returns the type name for both bare-Path and TypeExpr forms, then unify the Heap/Shared/List/Map /Set intercepts and the qualified-function lookup so they consume it from a single source.
End-to-end: Shared<Int>.new(42), Shared<Bool>.new(true),
Shared<Text>.new("hello"), Heap<Int>.new(7) all run cleanly in
the interpreter. The Shared<T> entry is now removed from
KNOWN_ISSUES.md — only AOT async, REPL evaluation, and the
by-design GPU/FFI/vmap interpreter fallbacks remain.
Fixed — AOT slice-op fat-ref loads route through as_ptr
SliceGet (0x06), SliceGetUnchecked (0x07), SliceSubslice (0x08),
and SliceSplitAt (0x09) in verum_codegen/src/llvm/instruction.rs
unconditionally called .into_pointer_value() on the register value
— panicking with "Found IntValue … expected PointerValue variant"
whenever the register held a NaN-boxed i64 encoding of the pointer
(exactly how the stdlib slice path stores the fat ref after a
Pack). Route all four sites through the existing as_ptr(ctx, val, name) helper, which already handles PointerValue, IntValue
(via int_to_ptr), and StructValue cases.
This was the primary L0 AOT blocker — make test-l0 previously
SIGABRT'ed at spec ~400 inside vtest-diff-aot. After this fix,
examples/showcase.vr builds and runs cleanly, and L0 proceeds
through ~2000+ specs before the residual LLVM stability work.
Fixed — interpreter runs __tls_init_* ctors before main
The VBC codegen emits a __tls_init_<NAME> synthetic function for
every @thread_local static and registers it in
module.global_ctors. The AOT path consumes these via
@llvm.global_ctors, but the interpreter was skipping
module.global_ctors wholesale (to avoid declared-only FFI library
initializers crashing on macOS). Skipping the TLS subset of those
ctors left @thread_local static slots uninitialised; TlsGet
fell back to Value::default(), which is not the declared initial
value. A Maybe<LocalHeap> stored as None then read back as
untagged zero, misfired the Some/None pattern-match, and the CBGR
allocator bootstrap crashed on the first Shared::new(...) with
"Expected int, got None" at value.rs:892.
Fix: selectively run only ctors whose function name starts with
__tls_init_. FFI library initializers keep their existing skip.
CompilationPipeline::phase_interpret calls the new
interpreter.run_global_ctors() before execute_function(main).
Verified: @thread_local static mut COUNTER: Int = 42; now reads
back as 42 inside the interpreter (was raw zero / panic before).
Added — CLI verify --solver={auto|portfolio|auto|capability}
The --solver flag on verum verify was defined with default
"smt-backend" but the value was only used for display — the verification
path hard-coded the SMT backend. Plumb the selection through to CompilerOptions
and log it from VerifyCommand so the runtime path can route
accordingly, and reject typos loudly instead of silently defaulting.
CompilerOptionsgainssmt_solver: BackendChoice(defaultBackendChoice::the SMT backendto preserve historical behaviour).verum_cli::commands::verify::SolverChoiceenum +parseso validation remains available even when theverificationfeature is disabled (that feature gates theverum_smtdependency and the realBackendChoice).- Unknown values like
--solver=foonow error with"Accepted values: auto, auto, portfolio, capability". VerifyCommand::runemits an info-level log naming the selected backend and timeout.
Actual backend routing (the SMT backend / portfolio / capability-router) is a
follow-up; the smt-backend feature ships in stub mode and transparently
delegates to the SMT backend inside SmtBackendSwitcher, so --solver=smt-backend
produces the SMT backend-equivalent answers in the default build.
Added — LSP choice-snippet completion for attribute enum values
@inline(<TAB> previously inserted the generic placeholder
"identifier" at position $1, because ArgSpec::Required(ArgType ::Ident) has no notion of the specific allowed identifiers. The
LSP completion layer now hard-codes the set of known choice-valued
attributes and emits an LSP choice snippet so editors offer the
allowed values inline:
@inline→always | never | hint | release@repr→C | packed | transparent | cache_optimal@optimize→none | size | speed | balanced@device→cpu | gpu
Chore — zero rustc warnings in cargo build --workspace
Eliminated 25 dead_code warnings that accumulated across
verum_smt::backend (stub-mode SmtBackend / SmtSort /
SmtModel / SmtResult + SMT_KIND_* constants kept for API
parity with the smt-backend-ffi build), verum_vbc::codegen:: get_current_ffi_platform (reserved for FFI signature generation),
and verum_vbc::interpreter::kernel::MIN_GPU_SIZE (CPU-vs-GPU
kernel-selection threshold). Each site is annotated with a narrow
#[allow(dead_code)] and a comment explaining when the code
becomes live.
The unit CI job now runs RUSTFLAGS="-D warnings" cargo build --workspace --locked as a blocking gate, so any regression
reintroduces a failing build.
Infrastructure — CI restored + production-readiness docs
.github/workflows/ci.ymlblocks on: unit tests (Ubuntu + macOS-14 aarch64), VCS L0 (2963 specs, 100%) + L1 (499 specs, 100%), Tier 0 vs Tier 3 differential (204+ specs).rustfmt --checkandclippy -D warningsrun advisory pending the one-shot reformat / manual clippy polish pass..github/workflows/nightly.ymlruns the full VCS sweep with cross-tier differential, 60-minute fuzzer across all targets, and benchmark comparison vs baseline.KNOWN_ISSUES.mdrewritten to reflect the current state — stale entries about@thread_local, byte-writes, and Text equality removed. Subsequently theShared<T>allocator crash was traced and fixed (see "Fixed —Shared<T>::newlowering" below); the remaining items are AOT async executor, REPL evaluation, and by-design GPU/FFI/vmap interpreter fallbacks.- New
CONTRIBUTING.mdwith pre-PR verification commands that mirror the CI gate (RUSTFLAGS="-D warnings" cargo build,cargo test --workspace --lib --bins,make test-l0 test-l1). vcs/baselines/l0-baseline.mddocuments the current 98.4% L0 compile-time pass rate and the reproduction path for the residual full-L0 AOT SIGSEGV.
[0.32.0] — 2026-04-15 — phase D complete
Major
- Cubical normaliser with computational univalence landed. Eight
reduction rules in
cubical.rs; bridge intounify.rsforType.Eq. Computationaltransport(ua(e), x) ≡ e.to(x). - VBC cubical codegen. New
CubicalExtendedopcode (0xDE) with 17 sub-opcodes coveringPathRefl,PathLambda,Transport,Hcomp,Ua,Glue, and friends. Proof erasure in release mode — cubical ops compile to identity / passthrough. - Proof-carrying bytecode. VBC archives embed certificates via
verum_smt::proof_carrying_code. Consumers can re-verify offline without running the full compiler. - Capability-based SMT router. Obligations classified by theory use; the SMT backend handles LIA/bitvector/array; the SMT backend handles strings / nonlinear / SyGuS / FMF. Portfolio mode cross-validates.
- θ+ unified execution environment. Memory + capabilities + recovery + concurrency form a single per-task context with spawn/await propagation.
- Incremental compilation fingerprinting. Function / type /
dependency / config hashes;
target/.verum-cache/per-project. Typical 10–15× incremental-edit speedup.
Added
@verify(thorough)and@verify(certified)— dual-solver execution.@verify(certified)— requires proof term; machine-checked.is_reflectable()gate for@logicfunctions (pure + total + closed).Tensor<T, const S: Shape>static shapes with shape-polymorphic operations; shape errors at compile time.math.agent— LLM-adjacent primitives (tokeniser, KV cache, speculative decoding, ReAct, guardrails, RAG).theory_interop— theory registry for formally-represented theories; Yoneda loading, Kan-extension-based translation, descent coherence.- Terminal UI framework (
core::term) — 7 layers from raw termios to Elm-architecture apps. - 800+ runtime intrinsics documented in
core::intrinsics. - Contract literals (
contract#"...") with compile-time SMT verification.
Changed
- CBGR dereference optimised to 11.8–14.5 ns (measured on M3 Max). Target < 15 ns — achieved.
- Stdlib collections: Swiss-table-backed
Map<K,V>replaces open-addressing implementation. - VBC opcode count reached 200+ (was ~150).
- Default SMT timeout raised from 2 s to 5 s for better portfolio convergence.
- Parser: switched to
verum_fast_parser(recursive descent with lossless green tree) as default;verum_parserretained for backward compatibility. @extern("C")blocks now acceptcalling_convention = "..."for non-default ABIs.
Fixed
- Generation wraparound race condition — epoch counter now advances cooperatively per-thread; hazard pointers protect available reads during free.
- The SMT backend 1.3.3 integration — brings bug fixes to string operations.
- Refinement narrowing across control flow:
if x > 0 { ... }correctly strengthensx: InttoInt { self > 0 }inside the branch. - Proof cache invalidation triggers solver upgrade — previously cached results were trusted across solver versions, leading to stale verdicts.
Deprecated
r#"..."#Rust-style raw string — use"""..."""(triple-quote) for multiline raw text.size_of<T>()/align_of<T>()intrinsics — prefer type propertiesT.size,T.alignment.
Tooling
- LSP: refinement-type diagnostics with counter-examples; CBGR
reference-tier hints (
&T/&checked T/&unsafe Tshown inline); quick-fixes for auto-import, protocol method generation,@verifyannotation. - Playbook TUI: session replay; context binding; inline verification diagnostics.
- CLI:
verum analyze --escape | refinements | smt | capabilities;verum smt-stats;verum expand-macros;verum target install <triple>. - Package registry:
verum publish,verum search,registry.verum-lang.org; content-addressed storage with IPFS support.
Benchmarks
Measured on Apple M3 Max, Verum 0.32 release build:
| Operation | Cycles | ns |
|---|---|---|
&checked T deref | 2 | 0.5 |
&T CBGR check | 55 | 13.8 |
Shared.clone (incr. strong) | 11 | 2.7 |
Map.insert (single) | ~200 | ~50 |
| context-stack push | 32 | 8 |
current_env() read | 8 | 2 |
Verification statistics
Project-wide on the stdlib + conformance suite:
| Theory mix | Obligations | Median (ms) | p95 |
|---|---|---|---|
| LIA only | 2,100 | 8 | 35 |
| LIA + bitvector | 940 | 14 | 60 |
| LIA + string | 110 | 45 | 180 |
| Nonlinear (NIA) | 42 | 320 | 1,800 |
| Cubical / path | 18 | 120 | 400 |
Cache hit rate: 68% average on incremental builds.
Migration notes
From v0.31:
r#"..."#raw strings →"""...""". Automated byverum fmt.@verify(formal)semantics unchanged. Portfolio / certified are new, opt-in.- New type properties
T.size/T.alignmentare source- compatible;size_of<T>()still works but emits a deprecation warning.
From v0.30 and earlier: cubical types weren't available. No
migration needed for existing code; new Path<A>(a,b) type and
friends are additive.
Contributors
43 contributors over the v0.32 cycle. Session 22 was the biggest — CBGR optimisation to 11.8–14.5 ns shipped in that session.
[0.31.0] — 2026-02-28 — cubical foundations
Added
- Cubical type theory in
verum_types:Path<A>(a, b), interval endpointsi0/i1,hcomp,transport,ua. - Higher-inductive type syntax:
type S1 is Base | Loop() = Base..Base. cofix fncoinductive fixpoint; productivity analysis viacheck_productivity.
Changed
verum_types::infer2.66 M LOC after cubical integration.
Fixed
- Infinite loops in inference when HKT parameter unified against itself.
[0.30.0] — 2025-12-15 — dual-solver portfolio
Added
- The SMT backend (
smt-backend-sys1.3.2). - Capability-based router in
verum_smt::capability_router. @verify(thorough)attribute.
Changed
- SMT obligation format standardised on SMT-LIB 2.6 across both solvers.
[0.25.0] — 2025-10-07 — dependent types
Added
- Σ-types via
type T is n: Int, data: [Int; n]. - Π-types (implicit — dependent return types over parameters).
- Higher-kinded type parameters:
F<_>. @verify(formal)integration with dependent obligations.
[0.20.0] — 2025-07-22 — refinement-type SMT
Added
- Three refinement syntaxes: inline on type, on parameter, on field.
- The SMT backend integration via
verum_smt::backend. @logic fnreflection.where requires/where ensures/ loopinvariant/decreases.
[0.15.0] — 2025-04-09 — VBC-first
Added
- VBC bytecode with 150+ opcodes.
- VBC interpreter;
verum rundefault. - LLVM AOT backend via
verum_codegen;verum build --release.
Changed
- Compiler pipeline reorganised around VBC as the single IR.
[0.10.0] — 2025-01-19 — three-tier references
Added
&T,&checked T,&unsafe Treference tiers.- CBGR — capability-based generational references.
- Escape analysis; promotion to
&checked T.
[0.05.0] — 2024-10-12 — type system skeleton
Added
- Bidirectional type inference.
- Protocol system (
type X is protocol { ... }). implement P for Tblocks.- Semantic-honest types:
List<T>,Text,Map<K,V>, etc.
[0.01.0] — 2024-07-05 — initial public tag
Added
- Lexer (via logos).
- EBNF grammar v0.1 (~800 lines).
- Parser shell; can tokenise
.vrfiles. - Executable compiles
main()withprint("hello, world!").