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.
[Unreleased]
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.sys.raw.{
__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!").