Skip to main content

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_corenormalize_with_budget, normalize_with_axioms_budget, and normalize_with_inductives_budget had 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 plain normalize fired. 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 single normalize_core(term, &mut NormaliseCtx) driver in support.rs; ~750 lines of parallel structural recursion collapsed to one. New public normalize_full is the canonical "all features on" entry point.

  • CanonicalCert::expected_outcome + helperscrates/verum_kernel/src/canonical_battery.rs had its 24-cert battery's expected verdicts encoded twice: in the cert constructor's shape and in a parallel expected_verdict(id) arm-table lookup. Folded the verdict into CanonicalCert itself + added readability constructors CanonicalCert::accept(...) / ::reject(...). The free expected_verdict(id) is now a one-line shim over the battery walk; the previously-required every_id_has_an_expected_verdict sanity test became vacuous (you can't construct a cert without a verdict).

  • KernelRuleId::strict_intrinsic_suffix() — a hardcoded 7-arm KernelRuleId → strict-intrinsic-name mapping in kernel_registry.rs::strict_tag_of was moved onto the enum itself (alongside name() and full_list()) so adding a rule is a single-place change. kernel_registry.rs now calls rule.strict_intrinsic_suffix(); the free function is gone.

  • KernelRule::meta() consolidation — three parallel methods on the 38-variant KernelRule enum (name() / v_stage() / category()) collapsed into a single 38-arm meta() returning KernelRuleMeta. The legacy accessors become #[inline] projections. New KernelRule::full_list() mirrors KernelRuleId::full_list. Three new pin tests: meta_accessors_agree_for_every_rule, full_list_covers_every_variant, full_list_count_matches_soundness_corpus. Deprecated KernelRule::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" in code()

    • integer in docs_url()'s ordinal-mapping match) and could silently drift. Introduced AntiPatternBand { Core, Base, Mtac, CveAh } carrying the canonical band.season() mapping; AntiPatternCodeMeta { code, name, ordinal, band } with one 40-arm meta() 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.

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.

  • ProfilesSensitive (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 }.
  • Floorvalidate_params enforces 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 immediate core/ subdirectory (48 entries).
  • verum_modules::propagate_submodule_reexports — surface explicit Path/Nested re-exports inside a public submodule into the outer ExportTable.
  • verum_compiler::pipeline::compile_orchestration Pass 0 — recursive process_imports_recursive descends into nested ItemKind::Module bodies (anchoring super / self resolution at the outer current_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 self receiver) under both the instance-dispatch key and the static-dispatch key. Closes ~134 of 158 Text statics plus every static surface on List, 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 with mount core.* glob no longer bypasses the archive load.
  • 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 empty Text (was: returned Unit).
  • "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.

  • Walkerverum_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 by MAX_TRANSITIVE_DEPTH = 32. Visits surface as PeerVisit { path, shape, depth, intermediate(), terminal() } with a ControlFlow<()> return so callers can short-circuit.
  • Policy adaptersresolve_transitive_lifecycle_regressions (closes AP-024) and resolve_transitive_foundation_downgrades (closes AP-019) filter visits by depth ≥ 2, leaving the direct one-hop dimension untouched.
  • Pipeline wiringPhaseInputs gained two new fields (transitive_lifecycle_regressions, foundation_downgrades) that propagate into DiagnosticContext through run_arch_phase_one_with. Two new Session helpers (resolve_transitive_lifecycle_regressions, resolve_foundation_downgrades) snapshot the registry and dispatch into the kernel resolvers. run_arch_phase_for_attrs_registry_aware populates both fields from the parsed shape.
  • Pin tests — four new entries in crates/verum_kernel/tests/k_arch_v_alignment.rs raise 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_map at the protocol-variant emit site routes resolve_field_type_ref with proto-level
    • method-local + synthetic-Self IDs. Self lives at 0x4000+N; associated-type paths hash into 0xC000..0xFFFF.
  • resolve_field_type_ref extended to handle TypeKind::Function, TypeKind::AssociatedType, and TypeKind::Qualified — the fast-parser produces Qualified for Self.Item, not AssociatedType.
  • PathSegment::SelfValue recognition was load-bearing — the naïve PathSegment::Name filter 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):

  1. Rank2Function decode arm — encoder wrote discriminant 0x08 for rank-2 polymorphic function types (Transducer-shaped); the decoder hit InvalidTypeRef.
  2. Extended-opcode length prefix — 9 extended opcode families (Arith/Ffi/Math/Simd/Char/Cbgr/Text/ Mem/Log) encoded opcode + sub_op + raw_operand_bytes with no length information; the decoder returned empty operands and corrupted the stream pointer. Fix: encoder now writes varint(len) + bytes after sub_op; decoder reads the length and consumes bytes via the new decode_extended_operands helper. 9 interpreter handle_*_extended paths skip the varint after read_u8(sub_op).
  3. Missing decoder armsTlsGet, TlsSet, GetVariantData, GetVariantDataRef were encoded but had no decoder arms; sequential decoders fell through to the wildcard Raw and 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.
  • RoundingMode enum: HalfEven (IEEE 754 banker's rounding, the default eliminating +0.5 bias), HalfUp, HalfDown, Truncate.
  • DivByZero error variant; div(&self, &other, precision, mode) plus convenience div_round (HalfEven default) and div_trunc (precision = 0, Truncate).
  • Scale-aligning arithmetic via a 19-entry POW10 const table; full overflow detection (checked_add / checked_sub / checked_mul including i64::MIN sentinel handling).
  • Sign-aware long division on i64 coefficients with a sticky-bit tracker for round-half tie semantics: (-7) / 2 HalfEven 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 to FmtText. 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 ship Text and let PG run to_tsvector server-side. Constants PG_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 (deriving type_oid plus 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 → decimalverum_internal_i64_to_decimal (already shipped at 3a111f8e).
  • strtod — pure-IR strtod (3bbd867c).
  • f64 → decimalverum_internal_f64_to_decimal LLVM IR helper (this release). Bit-pattern NaN / Inf / zero special-cases, fptosi integer part composed with the i64 helper, frac handling via fptosi(frac × 1e6) plus 6-digit zero-pad div / rem loop with trailing-zero strip. emit_verum_float_to_text swaps snprintf("%g") for the new helper through the runtime bridge get_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 / Eq bound clauses, both arg orders, both unqualified and fully-qualified count_o_unbounded paths, 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_refinement pre-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_dispatch module with a focused pipeline: CountOQuery → SMT-LIB assertion → FmfQuery over an uninterpreted individual sort with cardinality ≤ K → the SMT backend model enumeration → count extraction from the pred_o definition (handles disjunctive bodies, true / false shorthand, and clamps at the discovered domain size).
  • Four-variant CountOResult orthogonal 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_dispatch integrates with the capability router by setting needs_finite_model_finding=true on the goal's ExtendedCharacteristics; 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 Aproof_checker.rs: bidirectional type-check with explicit substitution + WHNF (the trusted base).
  • Algorithm Bproof_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_kind reduces a 6-variant sum carrying table/column names to a single discriminant tag string ("no_such_table", "primary_key", …). The Display for AlterValidationError impl 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:

  • BoolBool::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 Noneunknown_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 bindings symmetrically with guard. The TypeTest exact-match arm strips both fields together since the specialized row no longer carries the conditional.
  • extract_guarded_patterns populates bound_vars from row.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 (Some when the pattern's outermost kind is PatternKind::Guard).
  • build_matrix peels the guard before recursing; specialization propagates the field through all 10 row-construction sites.
  • extract_guarded_patterns consumes 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> and target_features: Option<Text>. Default None; the AOT pipeline preserves the previous behaviour (host detection on native, "generic" / empty on WASM) when both are None.
  • cli::commands::build: precedence is --target CLI flag > [llvm].target_triple > host default; [llvm].target_cpu and [llvm].target_features populate the new options when unset. CLI doesn't expose --target-cpu / --target-features today; the manifest is the user-facing knob.
  • pipeline.rs::phase_generate_native: the host-detection branch now goes via cpu_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>]: new distributed_cache_trust field on both with the standard profile-merge precedence (with_profile overrides parent).
  • ProfileConfig::distributed_cache_trust: populated from merge_with_manifest, forwarded to CompilerOptions.distributed_cache_trust alongside the URL.
  • CompilerOptions.distributed_cache_trust: documents the three accepted policy strings ("all" / "signatures" / "signatures_and_expiry") and the unknown-value fallback.
  • VerifyCommand::new: when distributed_cache_url is set, constructs a CacheConfig::default().with_distributed_cache(...) carrying the parsed TrustLevel and uses VerificationCache::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 (default true): when false, suppress the stderr warning. CI pipelines that capture and report divergences through the routing-stats sink avoid duplicate noise.
  • fail_on_mismatch (default false): when false, return the the SMT backend result rather than a hard Error — the documented "log but don't fail" policy. The Certified strategy sets the flag true to 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:

  1. Immutable releases(name, version) is a permanent binding to a chain hash.
  2. Envelope integritychain_hash MUST match the canonical derivation blake3(input_hash || build_env_hash || output_hash). Tampering observable.
  3. 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 — referenced ErrorCategory (resolved to a different cog's type with different variants) instead of WeftErrorCategory. Test now uses the canonical name; the underlying API is unchanged.
  • vcs/specs/L2-standard/net/weft/json_extractor.vr — used Rust-style turbofish extract_json::<T>(...); Verum grammar uses the spaceless f<T>(args) form. Stripped the 12 occurrences. The module's user-facing extractor type was promoted from WeftExtractorJson<T> to the spec-canonical Json<T>, dropping the placeholder in handler.vr and the Json is JsonValue alias in core.encoding.json (which made JsonValue the single canonical value-tree name).
  • vcs/specs/L2-standard/net/weft/dst_basic.vr — used Rust-style lifetime annotation &'static Text on pub const PROPERTY_*. Verum form is Text. 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 its higher_is_better direction.

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 single Params value alongside the timeout — required because Solver::set_params is destructive).
  • memory_max_size and smt.random_seed → global the SMT backend params via set_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 via try_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, skip stats.record_pattern_generation so 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 in gpu_targets when 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.

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--verbose linker 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() -> bool accessor on GpuPassPipeline so 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.minimize the 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 (default num_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 public interpolation_enabled() accessor for the higher-level verum_smt::interpolation layer 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 runs formula.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.

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-reading EnterpriseConfig internals.
  • 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 in propagate_constant, the constant fast-path inside propagate_binop, and the merge-into-concrete path in propagate_phi.
  • enable_range_analysis — gates the range-refinement branch in propagate_binop.
  • enable_symbolic_execution — gates symbolic-mirror writes in propagate_constant, propagate_binop, and propagate_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 — myappMyapp).
  • Build: {app_name} {ver} (...) line of the human report (verbatim casing — matches verum --version style).
  • app_name field in the JSON envelope's environment block (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 (default true) — register_migration now rejects when disabled with a typed HotCodeError whose 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) — the replace path now compares the elapsed replacement time against the configured ceiling. A breach surfaces as a tracing::warn! so callers can react (e.g. by calling rollback); the replacement itself isn't undone since the new code is already live and rolling back mid-call is its own hazard.
  • atomic_replacement (default true) — 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)), record functions_skipped_smt for 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 (default true) — now sets the SMT backend's preprocess-only option (false → run full pipeline, true → stop after preprocessing).
  • quantifier_mode (default Auto) — Auto leaves the SMT backend's heuristic in place; the four named modes (None, EMatching, CEGQI, MBQI) pin a single strategy via the quant-mode option.
  • verbosity (default 0, range 0-5) — sets the SMT backend's verbosity option 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 gates project_onto_shared: if the elimination set exceeds the budget, the engine returns a typed error before invoking the QE tactic.
  • quantifier_elimination (default true) — when set to false, project_onto_shared skips the QE step and returns the original formula. Interpolation correctness is preserved on the McMillan-style A ⇒ I half but the precision of the I ∧ 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:

  • 0skip (identity tactic — no rewriting; chosen because composing with and_then later still works uniformly).
  • 1simplify only.
  • 2 (default) → simplify chained with propagate-values.
  • 3 and abovesimplify + 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:

  • SmtBackend gains a set_timeout_ms(&mut self, ms: u64) trait method with a no-op default so legacy backends compile unchanged.
  • RefinementChecker::check_with_smt and verify_refinement_with_assumptions call backend.set_timeout_ms(self.config.timeout_ms) before every query.
  • RefinementSmtBackend::set_timeout_ms overrides the trait method and forwards to its inner SubsumptionChecker::set_smt_timeout_ms.
  • SubsumptionChecker::check_smt configures the the SMT backend solver's timeout parameter via Params::set_u32 on every fresh solver instance, mirroring the existing pattern in QESolver::fresh_solver and 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 descriptorstype_params_count (≤ 64), params_count (≤ 256), ctx_count (≤ 32).
  • Constant::Array — element count bounded at MAX_CONSTANT_ARRAY_LEN = 1 048 576.
  • Specialization entriestype_args_count bounded at MAX_SPECIALIZATION_TYPE_ARGS = 64 (matches the generic-fn type-param cap).
  • Source mapfiles_count bounded at MAX_SOURCE_MAP_FILES = 65 536; entries_count bounded at MAX_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 the ast_to_type recursion cap that already gates the front-end)
  • MAX_FIELDS_PER_DESCRIPTOR = 4 096
  • MAX_VARIANTS_PER_DESCRIPTOR = 4 096
  • MAX_PROTOCOLS_PER_DESCRIPTOR = 256
  • MAX_METHODS_PER_PROTOCOL_IMPL = 4 096
  • MAX_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 576
  • MAX_FUNCTION_TABLE_ENTRIES = 1 048 576
  • MAX_CONSTANT_POOL_ENTRIES = 1 048 576
  • MAX_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 536
  • MAX_MODULE_NAME_BYTES = 16 KB
  • MAX_DEPS_PER_MODULE = 4 096
  • MAX_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-loaded Arc<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 / TypeId cross-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 / CallG has args.count checked against the target function's declared params.len().
  • Decoder failures mid-stream.
  • Content-hash tampering: blake3 over data[HEADER_SIZE..] recomputed and matched against the header's content_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 graceful None fallback 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_contract with the same realize= 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 Module to the cross-file phase via lint_one_with_cache → CorpusFile, eliminating the second parse per file. Cache-hit entries are re-parsed in a single batched pass.
  • parse-error meta-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 --fix and JSON fix.edits consumers 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.cbpvcbpv_occurs_free, capture-avoiding cbpv_substitute, CbpvStep outcome type, cbpv_step (β / force-thunk / sequence-bind with congruences), cbpv_normalise(t, gas) to fixed point, cbpv_alpha_eq.
  • core.control.continuationCcStep, cc_step (β / reset-value / shift-capture), cc_normalise(t, gas), cc_alpha_eq.
  • core.logic.linearlin_to_nnf (de Morgan + involutivity), lin_negate, lin_is_nnf, lin_eq, lin_size, lin_atom_count.
  • core.logic.kripkevalid_in_frame, semantically_equivalent, frame-property predicates is_serial / is_reflexive / is_transitive / is_symmetric / is_euclidean (modal axioms D / T / 4 / B / 5), is_s5.
  • core.types.poly_kinds — full Robinson kind_unify + kind_apply + kind_compose, plus is_concrete, kind_arity, apply_args, free_vars.
  • core.types.qttmul_quantity (multiplicative scaling under λ-binders), is_sub (subquantity lattice Zero ≤ One ≤ AtMost(n) ≤ Many), top_quantity / bottom_quantity, quantity_eq.
  • core.meta.tactic — recursive meta_normalise (bottom-up β-cancel + seq-elim), meta_is_normal, seq_eliminate.

Intrinsic safety contractscore/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/.

Encodingbase32 (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).

Securityhpke (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).

Collectionslru, ttl_cache, bloom, hyperloglog, count_min, reservoir (Vitter Algorithm R), consistent_hash (Ketama-compatible).

Basesnowflake, nanoid, semver, glob (fnmatch(FNM_PATHNAME|FNM_LEADING_DIR) semantics).

Timerfc3339 (ISO 8601 w/ Howard Hinnant date math), cron (POSIX 5-field with Vixie OR-semantics).

Metricsewma (fixed-α + Dropwizard time-decaying + RateMeter with 1/5/15-minute windows).

Asyncsemaphore (cooperative task limiter, RAII permit), backoff (exponential / decorrelated / Fibonacci with jitter).

Netcontent_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:

  1. Impl vars in for_type, in declaration order (positional binding slots, impl_var_count = block size);
  2. Impl vars outside for_type (left free, inferred from bounds or unification at the call site);
  3. 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 form first [t₁, t₂, t₃]. repeat, try/try … else, all_goals, and focus already accepted block bodies; first now does too. Enables ring_law, field_law, category_law and the full core/math/tactics.vr strategy 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 optional where clause 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 enclosing try/first combinators.
  • 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::crash module installs a process-wide crash reporter at verum startup. It captures every panic and every fatal signal (SIGSEGV, SIGBUS, SIGILL, SIGFPE, SIGABRT on Unix via sigaction + sigaltstack; SetUnhandledExceptionFilter on 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 (verum version, git SHA, profile, target, rustc --version), the thread name, a Rust backtrace, and a breadcrumb trail.
  • New verum_error::breadcrumb module — 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 at stdlib_loading, project_modules, load_source, parse, type_check, verify, cbgr_analysis, ffi_validation, rayon_fence, generate_native, codegen.vbc_to_llvm, and interpret.
  • New verum diagnose subcommand 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.gz suitable 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 via gh CLI 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 workspace Cargo.toml — inherits from release but keeps debug = "line-tables-only" + split-debuginfo = "packed" so crash-report backtraces resolve to file:line. The main binary size is unchanged; line tables live in an external .dSYM / .dwp bundle.

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:

  1. Eager native-target initverum_cli::main now calls Target::initialize_native as 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.
  2. Real rayon fence before LLVMrayon::yield_now() in phase_generate_native is replaced with rayon::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

[0.1.0] — 2026-04-17 — runtime foundations, first public version

Fixed — VBC + AOT byte-slice semantics

  • Text.as_bytes() and slice_from_raw_parts(ptr, len) now produce proper slice values in both tiers. The VBC lowering previously emitted Pack (a heap tuple) for slices, so .len() returned 2 (the tuple arity), bytes[i] returned an 8-byte NaN-boxed Value instead of a byte, and every CBGR slice op silently fell through its is_fat_ref() guard. New CbgrSubOpcode::RefSliceRaw (0x0A) builds a FatRef directly from (ptr, len) with elem_size=1. New TextSubOpcode::AsBytes (0x34) materialises a byte-slice FatRef from either the NaN-boxed small-string representation or the heap-allocated [header][len:u64][bytes…] layout; the codegen intercepts .as_bytes() on Text receivers and routes through this op so self.ptr via GetF (which reads the wrong offset for both representations) is never called at runtime.
  • Matching AOT/LLVM handlers lower TextExtended::AsBytes (reads the pointer via verum_text_get_ptr, reads len from field 1 of the flat {ptr, len, cap} struct) and CbgrSubOpcode::RefSliceRaw into the standard 40-byte Pack-object slice layout, so the AOT Len / GetE / IterNew handlers already in place pick up the fix without further change.
  • CbgrSubOpcode::SliceGet, SliceGetUnchecked, and SliceSubslice now honour fat_ref.reserved as the element stride (1/2/4/8 for raw integer arrays, 0 for NaN-boxed Values). Previously they hard-coded sizeof(Value) and walked 8 bytes per index, so indexing or subslicing a byte slice produced garbage.

Fixed — variant-tag consistency

  • Maybe<T> is declared None | Some(T), so register_type_constructors assigns None=0, Some=1 positionally. The hard-coded fallback table in codegen/mod.rs had Some=0, None=1 — the stdlib- constants pass ran first and register_function overwrote by arity, so None ended up tagged as 1 while pattern matching (which derives tags from declaration order) expected 0. Every None value silently matched Some(x) => arms and bound x to uninitialised payload memory. Tags are now consistent across all three sites (register_stdlib_constants, builtin registration around compile_program, register_type_constructors) and the runtime helpers (make_maybe_int, make_some_value, make_none_value).
  • Bare None identifiers were lowered through the __const_ path to LoadI 0 instead of MakeVariant tag=0. Zero-arity variant constructors now route through MakeVariant before the constant path.
  • TextExtended::AsBytes handler auto-derefs both CBGR register references and ThinRef before inspecting the Text layout, so s.as_bytes() inside a function taking s: &Text no longer returns an empty slice.

Fixed — slice method dispatch

  • implement<T> [T] blocks register under the Slice.* prefix (because extract_impl_type_name_from_type maps TypeKind::Slice(_) to "Slice"), but the method-dispatch codegen was formatting [Byte].method as the lookup key (using extract_type_name_from_ast). The two halves of the pipeline disagreed; method names now get normalised so […].methodSlice.method before interning.
  • core.collections.slice was present in the AOT-retention list but not in the primary ALWAYS_INCLUDE that controls which stdlib modules get compiled into the VBC module at all. Added, so the normalised Slice.* lookups actually have bodies to find.
  • Intercept [T].slice(start, end) at codegen and emit CbgrSubOpcode::SliceSubslice directly, bypassing the compiled stdlib body (which panics inside Value::as_i64 when it receives a FatRef receiver via CallM).
  • extract_type_name now handles TypeKind::Slice(T) → "[T]" instead of returning None, so method-chain inference carries slice-ness through calls like s.as_bytes() and downstream .slice() / .get() / .is_empty() dispatch can route correctly.

Fixed — stdlib parsing and Text layout

  • Text.parse_int / Text.parse_float route through the pure-Verum parse_int_radix(10) path instead of the legacy text_parse_int / text_parse_float intrinsics, whose runtime declarations returned Maybe<T> while the stdlib wrappers were typed as Result<T, ParseError>. Because Maybe::Some(n) has tag 1 and Result::Err(e) also has tag 1, every successful parse was read back as Err(n)"42".parse_int() returned Err. 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).
  • Text values have two coexisting runtime layouts under the same TypeId::TEXT:
    • static / intrinsic-built heap strings: [header][len:u64][bytes…]
    • stdlib builder Text {ptr, len, cap}: three NaN-boxed Value fields produced by Text.new() + push_byte. Both the Len opcode handler and the TextExtended::AsBytes lowering were decoding only the first case. For a fresh Text.new(), .len() read the null-pointer bit pattern as a u64 and reported 9221120237041090560; .as_bytes() produced a FatRef into random memory. Both now disambiguate by object size + field tags and report correct values for builder Text.

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 return Ok.

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 (was false)
  • built("a") == "a" and reverse → true (was false)
  • Map<Text, Int>.insert(built_key, 1) → no crash
  • JSON.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. IterNew already recognises TypeId::MAP / TypeId::SET and builds the right ITER_TYPE_MAP iterator, so wrapping the map in a second iterator object (the first attempt) only confused IterNew into treating it as a list.
  • IterNext for ITER_TYPE_MAP now yields (key, value) 2-tuples (heap-allocated TypeId::TUPLE objects) matching the shape the codegen destructures for for (k, v) in …. The same change applied to the method-level iterator dispatch next arm as a defensive layer.
  • is_custom_iterator_type now recognises the stdlib iterator wrappers (MapIter, MapKeys, MapValues, SetIter, ListIter, …) as "builtin-like", so for (k, v) in m.iter() goes through the IterNew / IterNext path rather than dispatching to the uncompiled MapIter.has_next / .next stdlib methods.
  • dispatch_primitive_method now accepts to_text as an alias for to_stringText (not String) is Verum's native string type and the stdlib uses .to_text() throughout (e.g. JSON's i.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 'List', found 'Int'". An earlier path inside infer_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 the 1 * and + 0 no-ops in the CbgrAlloc Ok-wrap, redundant as *mut u8 casts on already-*mut u8 pointers, collapse a nested if {…} inside a matches!-guarded layout-property branch, replace is_some + unwrap with let Maybe::Some(ref body) = ….
  • verum_smt: drop the redundant outer ..Default::default() in SmtConfig::debugging.
  • verum_types: invert the Layer PartialOrd/Ord pair so cmp is the canonical implementation; replace min(64).max(1) with clamp(1, 64) in BitVec::new.
  • verum_mlir: add a Default impl for LlvmContextRef.

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:

  1. 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 the Foo<T>.method(...) static dispatch unification — the local nums got lifted into a would-be nums.map static lookup.
  2. The interpreter's handle_call_method resolved the resulting bare "map" string by suffix-scanning the entire registered function table, and core/collections/map.vr's top-level pub fn map<K,V>(pairs) happened to register before List.map. The dispatcher picked it, ran into Map.with_capacityself.resize(cap), and the private Map.resize is 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.vrxs.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 to static NAME: TYPE = EXPR; (with type annotation) or const 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 via verum_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:

  1. Field access on an ExprKind::TypeExpr had no layout-property handler. SharedInner<T>.size and SharedInner<T>.alignment fell through to a generic field load that returned i64::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". Add try_resolve_type_layout_property (TypeExpr) and layout_property_for_named (bare-Path generic params and user structs). In VBC's NaN-boxed model, every record slot is exactly one 8-byte Value, so the answer is field_count * 8 for size, 8 for alignment, and the type arguments are layout-irrelevant.
  2. Method dispatch on a TypeExpr receiver fell through try_flatten_module_path (which only knows Path nodes) and compiled the receiver as a runtime value — emitting LOAD_K String("Type { kind: Generic { …") followed by SetF against garbage intern indices like r1.306. Extract a static_receiver_type helper 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.

  • CompilerOptions gains smt_solver: BackendChoice (default BackendChoice::the SMT backend to preserve historical behaviour).
  • verum_cli::commands::verify::SolverChoice enum + parse so validation remains available even when the verification feature is disabled (that feature gates the verum_smt dependency and the real BackendChoice).
  • Unknown values like --solver=foo now error with "Accepted values: auto, auto, portfolio, capability".
  • VerifyCommand::run emits 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:

  • @inlinealways | never | hint | release
  • @reprC | packed | transparent | cache_optimal
  • @optimizenone | size | speed | balanced
  • @devicecpu | 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.yml blocks 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 --check and clippy -D warnings run advisory pending the one-shot reformat / manual clippy polish pass.
  • .github/workflows/nightly.yml runs the full VCS sweep with cross-tier differential, 60-minute fuzzer across all targets, and benchmark comparison vs baseline.
  • KNOWN_ISSUES.md rewritten to reflect the current state — stale entries about @thread_local, byte-writes, and Text equality removed. Subsequently the Shared<T> allocator crash was traced and fixed (see "Fixed — Shared<T>::new lowering" below); the remaining items are AOT async executor, REPL evaluation, and by-design GPU/FFI/vmap interpreter fallbacks.
  • New CONTRIBUTING.md with 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.md documents 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 into unify.rs for Type.Eq. Computational transport(ua(e), x) ≡ e.to(x).
  • VBC cubical codegen. New CubicalExtended opcode (0xDE) with 17 sub-opcodes covering PathRefl, 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 @logic functions (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_parser retained for backward compatibility.
  • @extern("C") blocks now accept calling_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 strengthens x: Int to Int { 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 properties T.size, T.alignment.

Tooling

  • LSP: refinement-type diagnostics with counter-examples; CBGR reference-tier hints (&T / &checked T / &unsafe T shown inline); quick-fixes for auto-import, protocol method generation, @verify annotation.
  • 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:

OperationCyclesns
&checked T deref20.5
&T CBGR check5513.8
Shared.clone (incr. strong)112.7
Map.insert (single)~200~50
context-stack push328
current_env() read82

Verification statistics

Project-wide on the stdlib + conformance suite:

Theory mixObligationsMedian (ms)p95
LIA only2,100835
LIA + bitvector9401460
LIA + string11045180
Nonlinear (NIA)423201,800
Cubical / path18120400

Cache hit rate: 68% average on incremental builds.

Migration notes

From v0.31:

  • r#"..."# raw strings → """...""". Automated by verum fmt.
  • @verify(formal) semantics unchanged. Portfolio / certified are new, opt-in.
  • New type properties T.size / T.alignment are 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 endpoints i0 / i1, hcomp, transport, ua.
  • Higher-inductive type syntax: type S1 is Base | Loop() = Base..Base.
  • cofix fn coinductive fixpoint; productivity analysis via check_productivity.

Changed

  • verum_types::infer 2.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-sys 1.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 fn reflection.
  • where requires / where ensures / loop invariant / decreases.

[0.15.0] — 2025-04-09 — VBC-first

Added

  • VBC bytecode with 150+ opcodes.
  • VBC interpreter; verum run default.
  • 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 T reference 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 T blocks.
  • 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 .vr files.
  • Executable compiles main() with print("hello, world!").