Skip to content

VCG by AI - #69

Open
wadoon wants to merge 46 commits into
mainfrom
weigl/vcg
Open

wadoon wants to merge 46 commits into
mainfrom
weigl/vcg

Conversation

@wadoon

@wadoon wadoon commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Known semantic divergences (Java/JML)

Current implementation limits — where the encoded semantics differ from Java/JML. The soundness pass (doc/vcg-semantics-plan.md) has resolved the original items (each bullet marks its plan phase); the mode-specific string-model divergences below remain.

  • Multiple spec cases keep disjunctive semantics (P1.1) — JML behavior cases are unrolled but never merged: at a call site the caller must establish the disjunction of the per-case requires, may assume each case's ensures guarded by its own requires, and the assignable clause is the union across cases. \static_invariant_for folds only static invariant clauses, so instance invariants (which read this) no longer leak into static methods.

  • Binary numeric promotion in BOUNDED mode (P1.2) — byte/short/char operands are widened to int (long wins; char zero-extends) before every binary and unary operation (JLS 5.6), shifts promote the left operand, the Java conversions are applied at stores and casts ((byte)300 == 44, b += 300 truncates to 54, JLS 5.1.2/5.1.3), conditional-expression branches are promoted (JLS 15.25), and overflow / div-by-zero checks test the promoted width.

  • Unresolvable callees are rejected by default (P1.3) — a call whose callee has no JML contract and no body (unresolvable, or a bodyless native/abstract method without a contract) raises a loud diagnostic instead of silently returning an unconstrained result while assuming the call touched nothing. --allow-unrefined-calls / allowUnrefinedCalls opts back into a sound over-approximation: the caller-visible frame (this.*, arrays, field maps) is havoced and the result stays unconstrained.

  • Runtime exceptions are checked by default (P3.3) — the six runtime-exception checks (checkNull, checkDivision, checkIndex, checkCast, checkNegativeArraySize, checkStringIndex) are ON by default, so a PROVEN method is also NPE/CCE/index/division-free (KeY-style); the CLI gained --checks=all|none|default and the missing --check-overflow flag (overflow stays opt-in: BOUNDED wrap-around already matches Java, the check only flags wraparounds). Supporting model fixes: new T(...) results are axiomatized non-null, a first store to an array-typed receiver field ties the flat slot to the field-selector read, and String.length() is a lazily-declared model read.

  • Constructor contracts are applied at creation sites (P2.1) — new T(...) is now a contract call site when the constructor is in the verified unit and carries a JML contract: the caller must establish the disjunction of the per-case requires (a falsifiable "precondition of constructor" obligation), may assume each case's ensures guarded by its own pre-state requires, and may assume the constructed class's invariant; assignable is scoped like contractCall (the fresh object's own state is already unconstrained, only named aliasable caller-visible locations are refreshed; no assignable / \everything refreshes the reachable frame). Supporting fixes: locals carry their resolved declared type on the SSA term, so contract calls on local receivers (c.get()) read through the same identity-keyed field map the construction established. Uncontracted constructors (library classes, implicit default constructors, contractless bodies) keep the fresh-object-with-unconstrained-fields model — a sound over-approximation, mirroring the P1.3 contractless-with-body carve-out (reads on such objects are still guarded by P1.3).

  • Constant field / static final initializers are folded into the entry state (P2.2, partial) — a constant folder (literals in decimal/hex/octal/binary, long, char, boolean, unary + - ! ~, + - * / % << >> & | ^, comparisons, && || == !=, plus references into earlier-declared constants, JLS 8.3.3 textual order) pins the slot where every legal execution reaches entry with that value: static final constants for every callable (this also folds constants into reads), constant instance initializers when verifying constructors (JLS 12.5, initializer-before-body — an empty default ctor now establishes its fields and the class invariant), and final instance constants in instance methods. Intermediate results are checked against the signed range of the promoted type, so folded values never wrap and agree with both arithmetic modes (1 << 31 and >>> stay unconstrained). Not folded (documented, sound): mutable static fields, mutable instance fields in methods (a competing constructor may overwrite them), forward references, method-call/cast/array initializers, reference-typed initializers — those slots stay unconstrained as before.

  • Multi-dimensional array creation is rejected (P3.1) — new int[n][m] used to be silently encoded as a 1-D array of the base type (only the first dimension was bound); generation now refuses every array with more than one level (new int[n][n], jagged new int[n][]) with an UnsupportedFeatureException naming the construct at its source range. Reflective array construction (Array.newInstance) never needed an extra guard: it resolves to an external call without contract/body and is rejected by P1.3 (or over-approximated under --allow-unrefined-calls).

  • Unsupported constructs fail with structured diagnostics (P3.2) — a pre-query validation pass rejects what the VCG cannot model with UnsupportedFeatureException("unsupported: <construct> at line N") carrying the node's source Range: switch expressions (switch statements are modeled), lambdas, method references, anonymous classes, Java assert statements (the JML //@ assert form is supported), JML keywords outside the supported set (\fresh, \reach, \TYPE, …; whitelisted are \result, \old, \invariant_for, \static_invariant_for, \everything, \nothing, plus the quantifier/label binder node kinds), and enum types (an ordinal/constant model would be required). The Normalizer throws the same type for generic unsupported statements.

  • Arrays are ordinary (nullable-by-default) references (P4.2) — arr == null is a real comparison against a per-sort null-array constant $null_array_<sort>, dereferencing a possibly-null array (a[i], a.length, element stores) raises a nullcheck obligation, new T[n] results are asserted non-null, and JML modifiers steer reference nullability exactly as for objects: non_null locations (params = implicit requires, fields = type invariant asserted at entry, locals = store obligations) fold == null/!= null and skip deref nullchecks. Elements are non-null by default: every element store obliges e != null, and nonnullelements is the explicit statement of that default (the grammar has no nullable-elements keyword, so a nullable-element store cannot be expressed — documented deviation; element reads stay unconstrained, since folding them would break fresh-array elements — documented deviation). Supporting model fixes: reference-typed array element sorts now print as U (the declared reference sort, not the never-declared Object placeholder), array lengths use per-sort bv$length_<tag>/int$length_<tag> functions because SMT-LIB forbids overloading, alias frames only apply to same-sort arrays (Java forbids mixed-sort identity), and the non_null-declared array field case is covered (this.data != $null_array_... assumed at method entry).

  • Call sites are visible states for class invariants (P4.1) — every call to a non-helper instance method checks the receiver's class invariants before the call (a class invariant at call to … obligation) and re-assumes them after it (the callee's own exit discipline restored them): this-receivers fold \invariant_for(this), explicit receivers o.m(...) fold \invariant_for(o) — the receiver's statically-visible set from the existing \invariant_for machinery. \helper callees are exempt on both sides (helpers may break and need not preserve the invariants), static callees and classes without in-CU invariants add nothing, the pre-call check and the contract view share one receiver evaluation, and constructor sites keep the P2.1 constructed-invariant assumption (new is not a visible state for the caller's this). The post-call assume is load-bearing: after an assignable havoc whose ensures says nothing about the field, the caller still proves invariant-constrained asserts/exit obligations from the re-assumed invariant.

  • Common String operations become abstract-string model reads (P4.3, opt-in via --check-string-index) — equals, charAt/codePointAt, concat, length(), both indexOf overloads, substring (both overloads), startsWith(String) and replace(char,char) translate to reads of a character-code model over the reference universe. In UNBOUNDED mode the model is native: strContent : U -> (Seq Int) is the single content view and the operations compile to direct sequence-theory terms — length() = (seq.len (strContent s)), charAt = (seq.nth …), equals = content-sequence equality, concat/substring results are fresh non-null references bound by a ground (seq.++ …)/(seq.extract …) equality, startsWith = (seq.prefixof …), indexOf(String) = (seq.indexof … 0) — and string literals bind their whole content natively (seq.empty/seq.unit chain). BOUNDED mode keeps the quantified axiom model (stringLength/strval/steq/strcat/strindex/strfind/strsub/strsub2/strpre/strrep): the proven directions are the same, but the mode has no native sequence sort. Each operation declares its function and length-bounded axioms once, lazily; every quantified axiom carries an explicit :pattern (Z3's E-matching, not MBQI) and every content axiom is flattened to a single universal level over a pattern on the content-application term — so satisfiability-only searches stay instant. indexOf(char) = first position or -1 (completeness / found / absent / minimality axioms; the only operation whose content lemmas the native theory cannot derive, so strindex keeps its axioms in U mode too); indexOf(String) = substring search whose found/empty/absent consequences are provable (k == indexOf(t) ∧ k >= 0 ⇒ k + t.length() <= s.length(), indexOf("") == 0, a found result pins the matched suffix, a -1 rules out any in-range suffix-prefix claim); replace(char,char) keeps the EUF strrep (native seq.replace_all has no (Seq Int) instance). Documented limits: BOUNDED-mode char-level reads compare 16-bit extracts of the 32-bit codepoint model (char-equality claims don't follow from codepoint inequalities), and a falsifiable claim cannot ride a query that carries the strindex/strrep axioms (MBQI mirroring defeats instant SAT answers).

  • The JML \sum generalized quantifier is modeled (P6.1) — \sum int j; lo <= j && j < hi; a[j] with a bounded integer range and a direct array-read body compiles to a single lazily-declared recursive SMT function (define-fun-rec jmlSum a lo hi …) that unfolds along the index range (hi <= lo → 0, else jmlSum(a, lo, hi − 1) + a[hi − 1]), so KeY's firstTouch 08-Java5 for-each sum (maintaining s == \sum int j; 0 <= j && j < \index; a[j] and ensures \result == \sum … a.length) verifies in BOUNDED and UNBOUNDED mode alike. Every other shape is rejected with a structured UnsupportedFeatureException naming the binder (no array-read term; no upper bound; extra range conjuncts; non-monotone bodies — KeySumBadShapes). Supporting mechanics: the KeY loop-position pseudo-variable \index binds the type-tagged loop-counter register $idxN in loop contracts, \strictly_nothing joins the assignable whitelist, and BOUNDED \sum queries fold length terms of constant array symbols (bv$length sym → a shared implicit $len_<sym> constant plus one ground >= 0 assert). The fold is what makes such queries terminate: under the jmlSum recurrence axioms Z3's recursive-function instantiation stalls against an explicitly declare-consted length symbol (full timeout on KeyFor's invariant/variant checks) but answers unsat instantly with the same symbol used implicitly. Solver-answer walkers (VcgResult.runCheck and the test helpers) skip the interleaved (error "…unknown constant…") notices before every verdict; non-\sum BOUNDED queries and all UNBOUNDED queries stay byte-identical. All four KeY firstTouch teaching examples run the VCG end-to-end: 05-ReverseArray keeps honest UNKNOWN/FAIL verdicts, 06-BinarySearch and 10-SITA (commonEntry, swap) prove through their loop invariants, and 08-Java5 proves its \sum contract.

Remaining divergences:

  • indexOf(String) wrong-position falsifiability is UNBOUNDED-only — the minimality direction (stringIndexOfWrong: the searched needle occurs at 0, the claim says 1) is satisfiable and answers instantly in UNBOUNDED mode, where the query is axiom-free by construction (native seq.indexof; no quantified string axiom to satisfy while building the model). BOUNDED mode keeps the quantified strfind/strsub/strpre search ladder, whose MBQI mirror makes the falsifiable direction churn to the watchdog — the proven directions are unaffected and B-mode runs remain in the suite for them, but the wrong-position claim is not part of the B-mode suite. Everything else listed in the P4.3 bullet is a model read (native in U, axioms in B) under --check-string-index.

Design direction (recorded, not implemented): boxing the compared type to (Seq (Seq Int)) (boxIntVal) would let a string-valued substring view live inside the native sequence theory, the follow-up path if case-sensitive-suffix/regionMatches-style reasoning is ever needed.

Testing

  • VcgSemanticsTest (335 cases, incl. P1.1 disjunctive spec cases, P1.2 promotion fixtures, P1.3 unrefined-call rejection/havoc, P2.1 creation-site constructor contracts, P2.2 constant field/static-final initializers, P3.3 default-on runtime-check policy, P3.1 multi-dim rejection, P3.2 structured unsupported-construct diagnostics, P4.2 array-nullability fixtures — 8 proven incl. a non_null array field, 2 falsifiable: dereferencing a possibly-null array and storing null into an object array — P4.1 visible-state invariant fixtures: callWithInvariantHeld proven, breakInvariantBeforeCall falsifiable (the guardrail), relyOnInvariantAfterCall proven on contract and inline strategies (post-call assume load-bearing), helperCallExempt proven, receiverInvariantHeld/receiverInvariantBroken (explicit-receiver variants) — and P4.3 string-model fixtures: stringSelfEquals proven, stringCharAtIndexOf proven (an indexOf >= 0 result pins a content position), stringConcatLen proven in both modes, stringIndexOfAbsent proven (a reported -1 discharges a grounded charAt content read), stringCharAtUnknown falsifiable (unguarded first-position claim), substringLen/substringLen2, literalStartsWith and literalStartsWithBroken (provable / falsifiable literal-prefix claims), startsWithLengthBound, replaceLen, replaceContentAt0, stringIndexOfLen/stringIndexOfEmpty/stringIndexOfFoundSuffix/stringIndexOfAbsentSuffix (result-domain / empty-needle / found-suffix / absent-suffix consequences of indexOf(String), proven in both modes under the native-vs-axiom encoding), stringIndexOfString proven in U (native seq.indexof), stringIndexOfWrong falsifiable in U (minimality: needle occurs at 0, claim says 1; the B-mode run churns and is not in the suite) — and regressions for loop-exit flag scoping and KeY's binary search through the loop invariant). — New P6.1 fixtures: the \sum suite (sumInvariant/sumArray/sumBounded/sumBoundedArr under their loop strategies, and KeyFor's for-each sum proven in both modes — the BOUNDED INVARIANT run exercises the length fold), six unsupportedConstructs rows — four KeySumBadShapes (no array term, no upper bound, extra conjunct, non-monotone body) plus the element/range-domain binder rejections from the merged core feat: element-domain quantifiers over arrays, ranges and Iterables #83 (quantIterableDomain, sumIterableDomain — the domain would otherwise be silently dropped), and SITA3's commonEntry/swap proven with rearrange generation-only.

  • ExprTranslatorTest (137 cases), NormalizerTest (204), VcgResultTest (158), VcgFacadeTest (10), VcgTest (27), VcgGoldenQueriesTest — curated golden SmtQuery snapshots (UPDATE_GOLDEN=1 to regenerate; drift fails the build). New P4.2 goldens: nullableArrayBranch (U/B), nonNullParamArray, freshArrayNonNull, storeNonNullElement, derefNullableArray. New P4.1 goldens: callWithInvariantHeld (U/B), breakInvariantBeforeCall (U), relyOnInvariantAfterCall (U_CONTRACT/U_INLINE), helperCallExempt (U), receiverInvariantHeld/receiverInvariantBroken (U). New P4.3 goldens: stringSelfEquals, stringCharAtIndexOf (U_STR), stringConcatLen (U_STR/B_STR), stringIndexOfAbsent, stringCharAtUnknown, substringLen, substringLen2, literalStartsWith, replaceContentAt0 (U_STR) — plus the U_STR indexOf(String) set stringIndexOfLen, stringIndexOfEmpty, stringIndexOfFoundSuffix, stringIndexOfAbsentSuffix, stringIndexOfString (regenerated under the native sequence encoding; stringIndexOfWrong stays golden-free — falsifiable).

  • Supporting soundness fixes in tools:smt (portable overflow encodings, watchdog → condition-based waits, JAVA_OBJECT sorts print as U).

  • CLI end-to-end (VcgCommandE2eTest, 4 runs spawning the real CLI as a subprocess) and shell tests (tools/cli/shell-tests/vcg/{search,sum,sita,reverse}.txt): 06-BinarySearch and 10-SITA (--vcg-exclude for the weak rearrange contract) verify; 08-Java5 verifies in both bounded and --unbounded mode (11 conditions, 0 failing — the fixture now carries the explicit requires a != null the VCG needs), and 05-ReverseArray reports its honest 31 conditions / 2 failing with UNKNOWN (????) verdicts. The shell tester gained a sortOutput frontmatter mode (comparison over sorted, normalized lines) so nondeterministic solver-completion order cannot fail the fixtures.

  • CI solver alignment (82933ddf7) — the Tests workflow installs Z3 4.15.4 (was 4.13.3). Z3 < 4.15 cannot terminate on quantified bit-vector obligations such as 06-BinarySearch's postcondition (the \exists over array indices under the nested sortedness \forall): Z3 4.13.3 runs until the solver watchdog maps the two postcondition conditions to UNKNOWN (????), failing binarySearchIsVerifiedThroughItsLoopInvariant and the search.txt shell fixture. Reproduced locally with 4.13.3 on the pre-merge tip (unrelated to the merge with main or the P3.2 element-domain guard); 4.15.4 decides the query instantly and the full :tools:smt/:tools:vcg/:tools:cli suites plus all six shell tests pass on it. 4.15.4 is the newest Z3 the pinned KeYProject/setup-smt action installs on every matrix OS (4.16.0's macOS asset needs a newer action revision).

  • Windows jmltk classpath fix (4ee31f163) — the last CI flake was the Windows merge job's "Run shell scripts" step. The Windows start script must use the %APP_HOME%\lib\* classpath wildcard (cmd.exe cannot hold an explicit 115-jar classpath: %APP_HOME% expands to absolute paths, so any set CLASSPATH= line — even split across continuations — exceeds the 8191-character limit). Java expands that wildcard in directory-enumeration order, and the distribution shipped two com.github.javaparser implementations: the fork core (jmlparser-core-1.2.0-SNAPSHOT.jar) and KeY's proofjava fork (javaparser-core-3.28.0-K13.6.jar, a transitive of the LSP's key.core), whose ParserConfiguration lacks setProcessJml. On Windows the proofjava jar sorts first, so jmltk vcg crashed with NoSuchMethodError on every install (Linux start scripts list jars in dependency order and worked). The CLI runtime now excludes the org.key-project.proofjava group, so the distribution carries exactly one com.github.javaparser implementation and directory order cannot decide behavior. Verified: proofjava-first classpath reproduces the exact CI stack trace; the deduplicated lib\* wildcard proves all 20 binarySearch conditions; shell tests 6/6, :tools:cli:test, :tools:lsp:test and spotless all green.

@github-code-quality

github-code-quality Bot commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Code coverage overview

Languages: Java

Java / code-coverage/jacoco-ubuntu-latest-21

main b7e0773 weigl/vcg 13d34ba +/-
59% 61% +2%
View coverage of the 10 most impacted files.
File main b7e0773 weigl/vcg 13d34ba +/-
com/github/java...ierVisitor.java 54% 58% +4%
io/github/jmlto...tTermFactory.kt 52% 83% +31%
io/github/jmlto.../SExprParser.kt 0% 71% +71%
io/github/jmlto...cg/VcgFacade.kt 0% 85% +85%
io/github/jmlto...olver/Solver.kt 0% 90% +90%
io/github/jmlto...lkit/vcg/Vcg.kt 0% 91% +91%
io/github/jmlto...VerifyMethod.kt 0% 91% +91%
io/github/jmlto...prTranslator.kt 0% 93% +93%
io/github/jmlto...g/Normalizer.kt 0% 99% +99%
io/github/jmlto...cg/VcgResult.kt 0% 100% +100%

ℹ️ Calculations represent line coverage. Results may change as calculations are updated.


Updated November 03, 2026 13:23 UTC

@wadoon wadoon self-assigned this Sep 27, 2026
@wadoon

wadoon commented Oct 16, 2026

Copy link
Copy Markdown
Contributor Author

@unp1 @mattulbrich If you want, feel free to poke it. Officially, the AI starts believing in this thing. I would have my doubts.

@wadoon
wadoon changed the base branch from main to weigl/lsp October 25, 2026 20:13
@wadoon
wadoon added this pull request to stack #81 October 25, 2026 20:13
@wadoon
wadoon removed this pull request from stack #81 October 25, 2026 22:03
@wadoon
wadoon changed the base branch from weigl/lsp to main October 25, 2026 22:03
wadoon added 17 commits October 31, 2026 23:22
Parameterized tables for every binary operator in Int and BV arithmetic modes,
all literal nodes, and compound expressions; dedicated tests for casts,
instanceof, conditional/ite, array/null comparison special case, \result,
\old/old (pre-state environment), name/field handler dispatching, JML infix
operators, chained comparisons, JML quantifiers (forall/exists, range guards,
array and non-primitive binders, bound-variable shadowing), array creation
(length assertion vs. initializer), and constructor default handling.

Also fixes a latent bug: the JML keyword form \old(x) is lexed with a leading
backslash in the method name and was falling through to the call handler,
silently becoming an unconstrained constant. Both 'old(x)' and '\old(x)' are
now resolved against the pre-state environment.
VC-generation stage suite (VcgSemanticsTest.kt) covering proven, falsifiable
and rejected-generation cases in both UNBOUNDED (Int) and BOUNDED (BV32)
modes. Fixes uncovered while making the suite green:

- execAssign now binds variables phi-style (v' = ite(g, rhs, prev)) so
  statements on dead paths (after break/continue/return) keep their previous
  value instead of leaking a fresh unconstrained constant into loop merges
  and postconditions.
- setFlag and execReturn use the same phi form (hard equality) so abrupt
  completion flags are defined when their guard is false.
- array lengths are axiomatised non-negative; without it a BV32 length could
  take a huge negative value that passes 'len <= 4' under signed comparison,
  turning a binary-search bound into nonsense (unsound BOUNDED binarySearch).
- loop-exit merges fall back to an unconstrained constant of the correct sort
  ($undef_N) for locations absent from exit snapshots instead of makeFalse().
- array element sorts come from the array's declared sort (BV32 element in
  BOUNDED, Int in UNBOUNDED); bvsaddo/bvssubo/bvsmulo drop the extra width
  argument; IntArithmeticTranslator maps boolean/float/double primitives to
  their real SmtTypes.
Add 45 new corner-case fixtures and tests to the VC-generation suite:
- MIN/MAX literals and boundary arithmetic (intMinLiteral, intMaxLiteral,
  nearMax, boundedIncrement, maxOverflow, minUnderflowWrap)
- instanceof as a statement (stmtInstanceof, stmtInstanceofCount)
- return/continue/break combined with loops and try-catch-finally
  (returnInsideLoop, plainReturnInLoop, tryBreakContinue, continueCountOnly,
  nonNormalLoopExit, nestedTry, tryReturnFinally)
- falsifiable-by-design wrong-spec fixtures (returnEarlyNoContinue,
  tryBreakOnly, continueThenReturn, loopContinueNoTry, loopBreakNoTry,
  nestedCatchFinally, minUnderflow)
- dedicated tests documenting that boxed parameter unboxing and boxed
  return types are rejected at generation time

Engine fixes:
- declare the String type sort in the hierarchy and qualify 'String' in
  instanceof translation (previously emitted an undeclared sort_string)
- reject boxed return types (Integer etc.) up front with a clear error
  instead of emitting an ill-sorted query
Drive VcgResult.kt to 100% line / 100% branch coverage:

- parameterized verdict mapping over the shared fixture bank with a stub
  solver: all-unsat -> PROVEN (95 rows, also asserting memoization and
  failedConditions reuse), all-sat -> FAILED (17 rows, failedConditions
  returns every condition), all-unknown -> UNKNOWN (20 rows)
- dedicated checks of the verdict loop edge cases: mixed answers mapped in
  order, answers shorter/trailing/empty/malformed-error tokens, empty
  condition lists, memoized single solver invocation, copy() giving an
  independent cache, fresh results re-checking, Status enum, the
  VerificationCondition data class (components/copy/equality/hash/toString/
  range default), and toString on both populated and empty results
- checkProgressive: live+final streaming with correct totals, default-arg
  bridges, cancelled runs that leave unanswered conditions UNKNOWN and are
  NOT memoized, explicit timeout and failed-verdict reporting
- end-to-end runs against the real Z3 solver (guarded by an installation
  assumption) for a proven, a falsifiable and a constructor fixture
Coverage for tools:vcg now at 97.85% instr / 98.52% line / 88.40% branch
across the module, with the ir/ package fully covered (100%).

Production:
- Remove NfBlock and Statement.asNfOrigin from ir/NfStmt.kt; the IR never
  constructed a block, and all three walk sites in Vcg.kt (execStmt,
  modifiedLocations, collectCalleeDeclaredKeys) plus Normalizer.kt were dead.
- Drop now-unused imports in Normalizer.kt and NormalizerTest.kt.

Fixtures (VcgExamples.java) and rows (VcgSemanticsTest):
- makeArray: `return new int[n]` covers ExprTranslator's ArrayCreationExpr
  visit and the anonymous-array naming (a declarator initializer would be
  cloned by the normalizer and detached, breaking calculateResolvedType).
- returnsUnknown: unresolvable return type -> returnTypeOf fallback.
- catchUnknownType: local declared with an unresolvable type -> tryResolve
  fallback through the assignment-target sort path (an unresolvable *catch*
  type would break the catch-match guard, which emits an undeclared sort).
- callInlineSwitch/helperSwitch + rejection test: switch inside an inlined
  callee is collected by declared-locals walk before being rejected.
- readUnknownField: host-selector fallback for a missing field on a resolved
  receiver, covering the backwards-sort fallback branch.

Tests:
- ExprTranslatorTest.testPrimitiveCastIsElided / testReferenceCastEmitsCastTerm
  cover both cast branches (elide vs. `(cast value sort_C)`).
- switch-inline rejection, options lookup/value semantics, and
  orphan-callable context rejection tests cover the remaining option and
  context helpers.

Suite: 682 tests green (132/202/158/163/27).
The VCG overflow obligations used Z3's bvsaddo/bvssubo/bvsmulo predicates,
which do not exist in the Z3 4.8.12 that the CI installs via apt on Ubuntu
24.04, so the solver rejected the query and VcgSemanticsTest's bounded
overflow cases (boundedAdd/boundedMul/boundedSub, overflowDetected,
boundedOverflow) failed. Encode signed overflow portably instead: sign-extend
both operands to a width in which the exact result cannot wrap (w+1 for +/-,
2w for *), compare the exact result against the sign-extended Java wrap-around
result, and flag overflow when they differ. Adds SmtTermFactory.signExtend.
… string index

Add four opt-in implicit-exception checks, each gated by a VcgOptions flag
and a matching VcgCommand CLI option:

- nullcheck: guard -> receiver != null on field-access receivers and
  instance method-call receivers (arrays exempt: encoded as value maps)
- castcheck: guard -> instanceof(v, sort_T) for reference casts; new T(...)
  binds typeof/instanceof of the fresh object so casts of fresh objects prove
- negative-array-size: guard -> dim >= 0 on new T[n] (always provable under
  the non-negative array-length axiom; test asserts emission + provability)
- string-index: engine-internal String/StringBuilder specs (charAt,
  codePointAt, substring); stringLength function over U, String.length()
  modelled as a read, declared string-literal constants with length asserts

Also folds in earlier uncommitted branch work: VcgFacade/VcgCache with the
<fqdn>#method#kind-N@line:col condition-id convention, jmlstub JRE stub
regeneration, and the SmtTermFactory idivide soundness fix for Z3 4.8.x.
…non-null

The report of `jmltk vcg` is now emitted as soon as a callable's conditions
have been solved instead of after all callables finish: VcgFacade gains a
callback overload of verifyAndCheckAll that invokes the consumer from the
worker thread (completion order) and still blocks until every callable is
done; VcgCommand prints each outcome under an output lock so lines of one
callable stay contiguous and the totals stay exact.

String literals are now declared reference constants (declared once per
value, asserted non-null) whenever a receiver-sensitive check is active, so
`--check-null` on a literal receiver is provable instead of spuriously
FAILing, identical literals no longer emit duplicate declarations, and
`--check-string-index` still binds each literal's runtime length.
The solver watchdog used to wake every 100 ms and re-check the cancel flag and
deadline. It now waits on a ReentrantLock Condition: the run loop signals it
after every parsed answer (so a fresh cancellation is picked up at the latest
when the next answer arrives) and shutdown() signals on teardown, while the
deadline is handled exactly by awaitNanos. No timer-based polling remains:
the watchdog wakes only on progress, shutdown, or deadline.
Model array `==`/`!=` as reference (identity) equality instead of extensional
content equality: a store through one alias is exactly a store through the
other, so when the identities match the last write wins. Identities are plain
`U` values produced by the lazily-declared `$arrayref_<elem>` functions, so the
solver can decide aliasing without extensional (content) reasoning.

- `==`/`!=` on arrays translates to identity comparison in both the VCG atom
  and the expression translator (wired through `arrayRefHandler`).
- every element store frames the other live array values with
  `ite(refEqual, store, old)` and preserves their lengths and identities;
  the stored array keeps its identity too.
- copying an array reference (`y = x`) copies the identity, and if-merges emit
  identity phis so a merged array is whichever branch was taken.
- fixtures: aliasArrayParams (the `a==b ==> a[0]==2` case), aliasArrayParamAliased,
  aliasArrayLocal, aliasArrayTwoLocals, inlineAliasedTwoArgs/bumpBoth.
Scalar/object instance fields of arbitrary receivers (parameters, locals,
`this.<field>` objects) are versioned `(Array U T)` heap maps keyed globally by
`fld$<tag>.<field>` and SSA-versioned like a local: a write is
`map' = ite(g, store(map, receiver, v), map)`, a read is `select(map, receiver)`.
Must-alias references (`b2 = b`) share one `U` identity, so `b2.value = 5`
is observable as `b.value == 5`; distinct objects observe nothing.

- reads of `scope.field` on arbitrary receivers prefer the identity map, then a
  per-receiver slot, then an unconstrained base (never the enclosing-class type).
- `this`-receiver writes keep the flat per-path slot but also update the map, so
  inlined/contracted callee receivers and `this` aliases observe them.
- assignable `this.f` clauses on receiver-bound contract calls refresh the map
  entry at the receiver; `\everything`/no-assignable havocs version every map.
- loop merges and invariant havocs version field-map keys with their printable
  `(Array U …)` sort (the generic printer would emit the invalid `Object`
  placeholder), and `modifiedLocations` records map keys for field writes.
- fixtures: aliasObjectLocal, aliasObjectTwice (proven in both modes).
Reads of `this`-receiver fields (explicit `this.f`, bare `f`) now go
through the identity-keyed field map whenever it is live, so a write
through a must-alias of the enclosing instance (`VcgExamples alias = this;
alias.val = 9` or a parameter assumed `alias == this`) is observable as
`this.val` / bare `val`. Previously such writes updated only the map
entry of the alias and this-receiver reads kept returning the stale flat
slot — the documented limitation.

To keep the flat and map models consistent:
- bare writes to fields of the enclosing instance (`counter = 0`
  normalizes to NfLocal) now update the field map like explicit
  `this.f = v` writes;
- declaring a field map anchors `select(base, this)` to the flat
  `this.<field>` entry, so requires/entry assumptions on a this-field
  survive reads switching to the map;
- loop merges (unroll final merge, invariant havoc) version this-receiver
  field-map keys like any modified location, and the invariant step-3
  havoc declares map sorts with the printable `(Array U ...)` sort;
- `assignable this.f` contract clauses refresh the map entry even without
  a receiver (plain `assigns f` on the enclosing instance), matching the
  receiver-bound case.

`\old` resolution is fixed along the way: the this-read handler now takes
the environment map explicitly so `old(...)` translates against the
pre-state instead of the current state.

New fixtures `aliasThisViaLocal`, `aliasThisViaParam`, `setValViaAlias`
are proven in both UNBOUNDED and BOUNDED modes.
Class-level `invariant` clauses are gathered from the context (the enclosing
type and its statically-visible ancestors in the same CU) and folded by a single
code transformation into one large conjunction in which every clause is wrapped
in a `\lblpos` label that keeps its identity:

  (\lblpos inv_1 e1 && \lblpos inv_2 e2 && ...)

The folded expression is added to the entry assumptions (JML: a non-helper
instance method may assume `\invariant_for(this)`) and to the exit obligation
(every instance method and constructor must preserve it). `helper` methods and
static methods are exempt. Literal `\invariant_for(..)` /
`\static_invariant_for(..)` expressions - parsed as `\`-prefixed method calls -
expand to the same fold, with a non-`this` receiver bound as the invariants'
`this`. `\lbl`/`\lblpos`/`\lblneg` label nodes are transparent to translation.

Also fixes the unknown-field/unknown-name fallback to be mode aware (BV32 in
bounded mode), which surfaces when invariants read fields of a superclass that
the engine does not track as flat slots.
Tie the havoced return/break/continue flags to 'the body did not return'
on the loop's normal-exit path, so post-loop statements are not silently
disabled (a return inside the loop must still allow the fall-through code
to run). Add regression coverage for both the invariant-abstracted loop
and KeY's binary search.

Derive the SMT array type from the element type in arraySmtType so that
new-array expressions on detached fragments (e.g. normalized new int[a.length])
resolve even when calculateResolvedType is unavailable.

Keep declarator/plain-assign initializer expressions attached to their
compilation unit instead of cloning, preserving symbol resolution.

Add a scratch harness (KeyReverseScratchTest) exercising the original
and normalized array-creation paths.
Prioritized (soundness -> completeness -> diagnostics -> design-level)
work plan for the known Java/JML divergences of the VCG, with code
locations, steps, tests, effort, and open decisions per item.
wadoon added 11 commits October 31, 2026 23:22
The six runtime-exception checks (checkNull, checkDivision, checkIndex,
checkCast, checkNegativeArraySize, checkStringIndex) are now ON by default
in VcgOptions, so a PROVEN method is also NPE/CCE/index/division-free
(KeY-style). checkOverflow stays opt-in: BOUNDED bit-vectors already wrap
exactly like Java, so the check only flags wraparounds (and is vacuous in
UNBOUNDED).

CLI: --checks=all|none|default selects the whole set; the missing
--check-overflow flag is added; individual --check-* flags enable a check
on top of the chosen policy. The LSP inherits the new policy via the
VcgOptions defaults.

Modeling fixes that the default-on checks exposed:
  - new T(...) results are axiomatized non-null (Java new never returns
    null), so a default-on nullcheck on a new T().m() receiver proves;
  - the first store to an array-typed receiver field (h.vals[i] = v)
    initializes the flat slot from the field-selector read instead of a
    fresh unconstrained array, so a default-on index check discharges
    against a requires h.vals.length >= n;
  - stringLength is declared lazily at first use (string-free queries no
    longer carry the declaration).

Tests: testRuntimeChecksOnByDefault replaces testRuntimeChecksOffByDefault;
testRuntimeChecksOffWithNonePolicy keeps the opt-out silent; arrayStoreInLoop
requires strengthened to a.length >= 3. Seven golden snapshots regenerated
with only the new default-on obligations plus obligation-id renumbering.
new T(...) is now a contract call site when the constructor is in the
verified unit and carries a JML contract:

- the caller must establish the disjunction of the per-case requires
  (a falsifiable "precondition of constructor" obligation at the site);
- each case's ensures is assumed guarded by its own pre-state requires
  (same discipline as contractCall);
- the constructed class's invariant is assumed (invariant_for(the fresh
  object) -- a valid constructor contract establishes it, exactly as
  checkPostcondition folds it into the constructor's own verification);
- assignable: fresh-object state is already unconstrained, so only named
  aliasable caller-visible locations are refreshed; no assignable or
  \everything (the JML default) refreshes the reachable frame.

The callee view binds this to the fresh object and exposes every
non-static scalar/object field of the constructed class as
select(field-map, fresh), declaring the maps eagerly so the clause reads
and the caller's post-construction reads share the same select terms
regardless of translation order.

Supporting fix: locals now carry their resolved declared type on the SSA
term (flatWrite), so receiver-bound views can expose the fields of a
*local* receiver (c.get() reads count through the same identity-keyed
field map the construction established); previously such reads fell back
to an uninterpreted constant.

Deviation from the plan sketch (documented in vcg-semantics-plan.md):
unresolvable constructors (library classes like new Object() / new
ArrayList<>(), implicit default constructors) are not rejected -- they
keep the fresh-object model with unconstrained fields, a sound
over-approximation that mirrors the P1.3 contractless-with-body carve-out
(reads on such objects stay guarded by P1.3).

Tests: useCountedCtor (both modes; construction then c.get() through the
same field map), ctorRequiresViolated (falsifiable constructor
precondition at the creation site), ctorInvariantUsable (body assert
discharges only through the assumed invariant); golden guardrails
useCountedCtor__U_CONTRACT and ctorInvariantUsable__U; 34 existing
goldens regenerated (only the new sort_counted type declarations).
…state

A constant folder (JLS-style constant expressions, fold-through of
earlier-declared constants in textual declaration order, JLS 8.3.3) pins the
slot where every legal execution reaches method entry with that value:

- static final constants bind for every callable (also folds constants into
  reads of the enclosing class);
- constant instance initializers bind when verifying a constructor (JLS 12.5:
  initializer-before-body - an empty default ctor now establishes its fields
  and the class invariant);
- final instance constants bind for instance methods (no constructor can
  rewrite a final-with-initializer field).

Not bound (documented, sound): mutable static fields, mutable instance fields
in methods (a competing constructor may overwrite them), forward references,
method-call/cast/array and reference-typed initializers.

Folder details: literal parsing follows JLS 3.10.1 (decimal vs hex/octal/binary
two's-complement typing, int vs long classification), unary + - ! ~, binary
+ - * / % << >> & | ^, comparisons and && || == !=. Every intermediate result
must fit the promoted signed range, so folded values never wrap and agree with
both BOUNDED and UNBOUNDED arithmetic; `1 << 31`, `>>>` and x/0 stay
unconstrained. Emission is mode-aware (BV width vs Int).

Tests: p22StaticFinalBlocks / p22ConstantShapes prove in U+B (were unprovable);
p22NonConstInit and p22ForwardRefInstance stay falsifiable; the ConstantInitCtor
fixture shows the constructor (int n) ctor entry binding, the final instance
constant read in a method, and the mutable-instance-in-method soundness boundary
(countAfterAnyCtor stays falsifiable). Golden guardrails: new
p22StaticFinalBlocks__U snapshot; 36 existing goldens regenerated with only the
new type constant, the new field slots/pins and the two new instance slots that
the everything-assignable contract frame havocs.
…g it

`new int[n][m]` used to be encoded as a 1-D array of the element type: only
the first dimension was read off the levels and the element sort silently
collapsed. Generation now rejects multi-dimensional array creation (any level
count > 1) up front with a structured UnsupportedFeatureException that names
the construct and carries the node's source range.

Introduces io.github.jmltoolkit.vcg.UnsupportedFeatureException (extends
UnsupportedOperationException) as the single diagnostic type for constructs
the VCG cannot model; P3.2 routes the remaining unsupported constructs
through it.

Reflective array construction (java.lang.reflect.Array.newInstance) needs no
extra guard: it resolves to an external call without contract or body and is
already rejected by P1.3, or explicitly over-approximated under
--allow-unrefined-calls.

Tests: testMultiDimArrayGenerationRejected covers multiDim (new int[n][n])
and the jagged multiDimJagged (new int[n][]), asserting the message and the
source range. Existing goldens unchanged - no fixture or golden case uses
multi-dimensional arrays.
A pre-query validation pass (validateSupported, run at verify() entry before
any query work) aborts generation on the first construct the VCG cannot model
with UnsupportedFeatureException("unsupported: <construct> at line N")
carrying the node's source range. Covered: switch expressions (only switch
statements are modeled), lambdas, method references, anonymous classes, Java
assert statements (the supported form is a JML //@ assert), JML keywords
outside the supported set (\\fresh, \\reach, \\TYPE and friends would
otherwise be silently mis-modeled or fail with an unrelated resolver error),
and enum types (verifying an enum member is rejected up front). The supported
keyword whitelist: \\result, \\old, \\invariant_for, \\static_invariant_for,
\\everything, \\nothing; quantifier/label binders (\\forall, \\exists,
\\sum, \\lbl, ...) are separate AST node kinds and are unaffected.

UnsupportedFeatureException (introduced in P3.1) is now the single diagnostic
type for unsupported constructs: the Normalizer's generic statement /
expression-statement / assignment-target throws use it too. It extends
UnsupportedOperationException, so existing assertThrows(UnsupportedOperation
Exception) guards stay green.

Tests: testUnsupportedConstructsRejected (lambda, method reference, anonymous
class, Java assert, \\fresh, \\reach), testSwitchExpressionRejected (a switch
expression only parses at a JAVA_12+ language level, so the probe parses a
JAVA_17 snippet; the shared fixture file stays at the default JAVA_11 level),
testEnumVerificationRejected (the Color enum appended to VcgExamples.java).
The new top-level Color type adds the sort_color type constant and subtype
edge to every golden query - a purely additive change, confirmed by diffing
the 37 regenerated goldens.
Arrays now behave like ordinary references: nullable by default, with the
JML modifiers steering nullability exactly as for objects. `non_null` is
the opt-out for a location; array elements are non-null by default on
every store; `nonnullelements` states that default explicitly.

Encoding:
- per-sort null-array constant `$null_array_<sort>` (declared lazily);
  `arr == null` is a real `(= arr $null_array_<sort>)` comparison instead
  of the constant-false folding
- deref nullchecks on `a[i]`, `a.length` and element stores; `new T[n]`
  results are asserted non-null; `non_null` locations fold the comparison
  and skip deref checks, with entry assumptions (params = implicit
  `requires`, fields = type invariant asserted at entry but not in
  constructors, locals = store obligations in flatWrite)
- object-array element stores obligate `e != null`; element reads stay
  unconstrained (folding would break fresh-array elements; documented
  deviation - the grammar has no nullable-elements keyword either)

Supporting model fixes: reference-typed array element sorts print as `U`
(the declared reference sort, not the never-declared `Object` placeholder
- smt golden updated accordingly), per-sort `bv$length_<tag>` /
`int$length_<tag>` length functions for non-int arrays (SMT-LIB forbids
overloading), alias frames restricted to same-sort arrays (Java forbids
mixed-sort identity), and a per-verification anonymous-array counter so
identical inputs yield identical queries.

Tests: 7 proven + 2 falsifiable array-nullability fixtures (incl. a
`non_null` array field holder, `nullableArrayBranch`, element-store
obligations), 6 new golden snapshots; VcgSemanticsTest grows 260 -> 277
and stays green. Goldens regenerated and diffed (mechanical: real
`a != null` plus `$null_array_*` declarations). Spotless + whole-project
compile green.
Every call to a non-helper instance method is a visible state: the
receiver's class invariants are checked as an obligation before the
call and re-assumed as an assumption afterwards (the callee's own exit
discipline restores them, mirroring checkPostcondition). this-receivers
fold the enclosing type's statically-visible invariants; explicit
receivers o.m(...) fold the receiver's set, both through the existing
InvariantFold/invariantForTerm machinery. Helper callees, static
callees, and classes without in-CU invariants add nothing. The pre-call
check and the contract view share a single receiver evaluation via
needsEarlyReceiver; the same discipline applies around inlined calls.

The visible-state pre-call check is the missing link between the
long-standing entry assumption (assumeClassInvariant) and exit
obligation (checkPostcondition): breaking an invariant mid-method and
restoring it only after a call is now falsifiable, and after a call
whose ensures says nothing about the havoced field the caller still
reasons about the invariant.

Tests: five new fixtures (callWithInvariantHeld proven,
breakInvariantBeforeCall falsifiable - the guardrail,
relyOnInvariantAfterCall proven on contract AND inline strategies with
a load-bearing post-call assume, helperCallExempt proven, and the
explicit-receiver pair receiverInvariantHeld/receiverInvariantBroken).
VcgSemanticsTest grows 277 to 282 green cases (KeY binary-search stays
green); new golden snapshots for all of them; among existing goldens
only useCountedCtor changes semantically (pre-check becomes v3, the
post-call assume is added, the exit postcondition renumbers to v4) and
the rest gain only the sort_invariantreceiver declaration line.
Closes the last Java/JML semantic gap in the VCG per the plan: under the
existing --check-string-index option, equals, charAt/codePointAt, concat
and the int(char) overload of indexOf become reads of a per-position
character-code model (steq/strval/strcat/strindex) instead of opaque
unrefined library calls, so String-heavy methods verify.

- equals is extensional equality cut by runtime length (both directions):
  s.equals(s) is provable, s.equals(null) is false (the argument stays a
  null-guarded reference)
- charAt/codePointAt read (strval s i) under the stringIndexChecker
  bounds obligation; statement-position reads carry the real path guard
- concat yields a non-null result whose length sums the parts and whose
  content prefixes the first argument, so s.concat(t).length() discharges
  its deref nullcheck
- indexOf returns the first position of the character or -1
  (completeness/found/absent/minimality axioms), with a char search
  argument upcast to the mode's int sort in BOUNDED mode

Each operation declares its function and length-bounded axioms once,
lazily, per operation behind the feature flag, so a charAt-only query
never pays for the steq/strcat/strindex axioms. The quantified axioms
carry explicit :pattern annotations so Z3's E-matching, not MBQI, drives
them; absence and minimality are flattened to a single universal level
over a strindex+strval multi-pattern, keeping satisfiability-only
searches instant (spiked: without this, MBQI churned near-trivial checks
to the 60s watchdog).

Five new semantics fixtures (282 -> 287 green): stringSelfEquals,
stringCharAtIndexOf, stringConcatLen (U+B), stringIndexOfAbsent proven;
stringCharAtUnknown falsifiable. Six new goldens. Documented limits:
BOUNDED-mode char reads compare 16-bit extracts of the 32-bit codepoint
model, and a falsifiable claim cannot share a query carrying the
indexOf axioms.
The 'Install Z3 SMT solver' step used apt-get, which only exists on
Ubuntu, killing the macOS and Windows jobs of the test matrix before any
test ran. Install Z3 with the cross-platform KeYProject/setup-smt action
(pinned to v0.3.1 by commit SHA), which downloads the official z3 binary
for the runner's OS and adds it to PATH; cvc5 is disabled as it is not
used.

Make the solver itself Windows-portable so the solver-backed tests in
:tools:smt, :tools:vcg, :tools:lsp and :tools:wd actually run and verify
on the Windows job instead of being silently skipped:

- Z3.kt: probe for z3 via 'where z3' on Windows instead of relying on
  'sh -c "which z3"'
- Solver: start 'z3 -smt2 -in' directly; ProcessBuilder resolves it on
  PATH on every OS (z3.exe on Windows), no shell wrapper needed
- SolverTest: the watchdog stubs used 'sleep 30', which does not exist on
  Windows cmd; use 'ping -n 31 127.0.0.1' there

Also force LF for the VCG golden *.smt2 snapshots in .gitattributes so
they stay byte-identical to the LF queries the generator emits on
Windows (same fix as the existing *.symbols rule).
- SolverTest: use a silent long-running stub on Windows (`cmd /c
  "timeout /t 30 /nobreak >nul"`). `ping` prints headers/replies to
  stdout, which the Solver's incremental SMT parser chokes on before
  the watchdog kills the process — the `timeoutKillsExactlyAtTheDeadline`
  / `cancellationDestroysTheSolverProcess` tests then fail on Windows
  with IllegalStateException.
- SmtQuery: serialize with a bare '\n' instead of PrintWriter.println
  (which emits the platform line separator). The generated query was
  CRLF on Windows while the committed .smt2 goldens are LF, so
  VcgGoldenQueriesTest drifted on Windows. Newlines are not semantic to
  SMT-LIB, so this only makes toString()/appendTo() byte-stable.
timeout.exe refuses to run when stdin is redirected, so the previous
stub (cmd /c "timeout /t 30 /nobreak >nul") exited immediately and
SolverTest.timeoutKillsExactlyAtTheDeadline returned before the 500 ms
deadline on Windows. Use cmd /c "ping -n 31 127.0.0.1 >nul" instead:
it stays alive ~30 s, writes nothing to stdout, and never reads stdin.
…3 extension)

Extend the --check-string-index abstract-string model with the remaining
common String reads so String-heavy methods verify:

- substring(i)/substring(i,j) ≈ strsub/strsub2: non-null slice whose
  runtime length is the slice width and whose content is the receiver's
  shifted by the start index (position-wise, like strcat); the call-site
  bounds obligations come from the existing stringIndexChecker.
- startsWith(String) ≈ strpre: the prefix relation in both directions —
  pre ⇒ len(p) <= len(s) and position-wise content agreement below len(p),
  and the converse proves literal-prefix claims from the literal's bound
  content. The argument is null-checked (s.startsWith(null) throws NPE).
- replace(char,char) ≈ strrep: non-null length-preserving result whose
  content maps the replaced character to the replacement (ite per position).

Dispatch restructured from a single-argument gate to per-op arity and
resolved-parameter checking (replace is gated to the (char,char) overload;
indexOf(String) stays unrefined/rejected by P1.3 — substring search over
the char view is the remaining documented divergence, pinned by the
stringIndexOfString fixture). String literals now bind per-position
content (strval(strlit,i) = utf16-code(value[i])) under the feature.

Z3 performance rule carried over from the indexOf spike: every content
axiom is flattened to a single universal level with a pattern on the
content-application term. A nested index quantifier whose index is the
arithmetic (+ s s) cannot be E-matched, so Z3 falls back to MBQI and
even a satisfiable bounds obligation (substringLen's string-index check
alone) churned to the 60 s watchdog; the flattened pattern keeps the
axiom inert until a ground content read appears.

Tests: substringLen/substringLen2, literalStartsWith(+Broken falsifiable),
startsWithLengthBound, replaceLen, replaceContentAt0, stringIndexOfString
(rejected); 4 new U_STR goldens; :tools:vcg:test fully green
(--rerun-tasks), spotlessCheck clean.
@wadoon
wadoon marked this pull request as ready for review October 31, 2026 23:20
@wadoon
wadoon marked this pull request as draft October 31, 2026 23:34
@wadoon
wadoon marked this pull request as ready for review October 31, 2026 23:39
@wadoon

wadoon commented Oct 31, 2026

Copy link
Copy Markdown
Contributor Author

All CI green on the new head fd7540325 (14/14 checks, incl. Windows/macOS test jobs). De-drafted.

The last commit extends the P4.3 abstract-string model (--check-string-index) with the remaining common String reads, so the divergences list now has exactly one entry:

  • substring(i)/substring(i,j) ≈ strsub/strsub2 (slice length = slice width, content shifted by start)
  • startsWith(String) ≈ strpre (prefix relation, both directions; argument null-checked — s.startsWith(null) throws)
  • replace(char,char) ≈ strrep (non-null, length-preserving, per-position ite content)
  • String literals bind per-position content under the feature, so literal-prefix/slice/charAt proofs discharge through the char reads.
  • indexOf(String) stays unrefined/rejected (P1.3) — it needs substring search over the char view; pinned by the stringIndexOfString rejection fixture (documented divergence).

Encoding detail for reviewers: every content axiom is flattened to a single universal level with a pattern on the content-application term (e.g. (strval (strsub s\$a s\$k) s\$j)). A nested index quantifier whose index is the arithmetic (+ s\$k s\$j) cannot be E-matched, so Z3 falls back to MBQI and even a satisfiable bounds obligation churned to the 60 s watchdog — the flattened pattern keeps such axioms inert until a ground content read is present (same rule as the indexOf spike).

Plan doc (doc/vcg-semantics-plan.md, P4.3 extension section) and PR body updated; :tools:vcg:test fully green with --rerun-tasks, spotlessCheck clean.

wadoon added 11 commits November 1, 2026 02:51
indexOf(String) is now modeled: (seq.indexof (strContent s) (strContent t) 0)
in UNBOUNDED mode, so the wrong-position claim (stringIndexOfWrong) is
falsifiable and answers instantly (no quantified string axiom in the query).
BOUNDED mode keeps the quantified axiom model (no native sequence sort over
bit-vectors); its proven indexOf(String) directions stay in the suite, its
falsifiable one is dropped (MBQI churn, documented).

UNBOUNDED mode compiles the modeled String operations to direct sequence
terms (length = seq.len, charAt = seq.nth, equals = content equality,
concat/substring = fresh non-null consts bound by ground seq.++/seq.extract,
startsWith = seq.prefixof), keeps quantified axioms only for strindex
(char search; the native seq.indexof cannot derive its content lemmas) and
strrep (replace(char,char); no seq.replace_all for (Seq Int)), and binds
string literal content natively (seq.empty/seq.unit chain). tools:smt gains
the Seq sort. 5 new U_STR goldens plus regenerated goldens; non-string U
goldens change only cosmetically (the eager receiver path blank-declares
strContent where base declared stringLength).
Mirror the real KeY teaching material (key.ui/examples/firstTouch) as
fixtures in VcgExamples.java and wire them into the semantics + golden
suites:

- ReverseArray.reverse/reverse2: quantified \old content invariants —
  generation-only (a list of exact obligations is asserted; the
  content-level \old reasoning exceeds the solver budget, so the queries
  are pinned by new golden snapshots instead of a Z3 verdict).
- BinarySearch.search: proven through the loop invariant (U_INV), with the
  KeY-assumed non-null parameter made explicit in the requires.
- Java5.For.sum: the contract uses the \sum generalized quantifier. The
  VCG now REJECTS non-\forall/\exists binders loudly (P3.2 fail-fast
  pass) instead of silently encoding \sum as a universal quantifier,
  which the real example exposed.
- SITA3.commonEntry/swap: proven through their loop invariants / old-based
  ensures (U and B for swap), again with explicit non-null requires.

Golden snapshots: new cases for reverse/reverse2/search/commonEntry/swap;
every existing snapshot gains the 4 new type sorts + Object-subtype edges
(add-only churn, regenerated with UPDATE_GOLDEN=1).
…ects (P5.1/P5.2)

Test coverage for every arithmetic/comparison/boolean operator through the
real translators in both modes (fold* + arithOps) exposed four encoder
defects, now fixed:
- '>>>' emitted bvshr (not an SMT-LIB function); now bvlshr — the logical
  shift — so -8 >>> 2 proves 1073741822, not -2
- shift distance not masked (JLS 15.19): translator binary() now applies
  (bvand d ((_ int2bv w) (w-1))); shiftDistanceMasking proves x << 33 == x << 1
- xor unconditionally Bool-xor built ill-sorted (xor bv bv); polymorphic
  SmtTermFactory.xor: Bool -> xor, BitVec -> bvxor
- makeLong used the raw literal string (L suffix): NumberFormatException on
  any long >= 2^31; shared JLS 3.10.1 longLiteralValue() parses radix/suffix

Bit-width ops have no encoding on mathematical integers: UNBOUNDED mode now
rejects << >> >>> & | ^ ~ loudly in both the runtime path (atom()) and the
expression path (ExprTranslator: contract clauses + JML asserts), instead of
a bare sort mismatch or a silently ill-sorted xor term.

Loop contracts: \returns clauses are enforced at an in-loop return
(LoopFrame.returns + execReturn obligation); a while(true) loop that leaves
only via break keeps the post-loop statements enabled (loopExitsOnlyViaBreak).

Rejections: block contracts (attached to BlockStmt) were silently dropped —
now rejected with a source-anchored diagnostic; labeled statements get a
dedicated message instead of the raw class-name fallback.

Tests: 16 new fixtures (foldIntRadixes/ArithOps/BoolOps/UnaryOps/ShiftOps/
DivZero/FieldSorts, arithOps u+b, bitwiseOps BOUNDED-only, shiftOps,
unsignedShiftNegative, shiftDistanceMasking, loopContractReturns[Wrong],
loopExitsOnlyViaBreak, stringCodePointAt, invariantForInBody); parameterized
proven/falsifiable/unsupported tables extended; ExprTranslatorTest goldens
updated (masked shifts, bvlshr, bvxor, 1L -> 1); 60 goldens regenerated
(add-only: 91 lines of AR_* entry-state declares each); whole-project suite
(860 vcg tests) + smt/wd modules + spotless + checkstyle green.
The atom() UnaryExpr bitwise-complement rejection was unreachable: every
fixture with a body-level '~' also contained a binary bitwise op (& | ^)
that threw first. bitCompOnly (~ in a body statement only) now exercises
it; the ExprTranslatorTest case for '~x' on Int sorts is tightened to
assert the structured message (it previously only checked RuntimeException).
…h examples

Spawns the real CLI (java -cp <test classpath> …MainKt vcg) as a subprocess on
the KeY firstTouch material adapted for the VCG (05-ReverseArray, 06-BinarySearch,
08-Java5, 10-SITA), with a hard-kill watchdog so a stuck solver cannot hang the
suite. Verdict signals are gated on Z3.z3Installed(), matching the in-process tests.

Assertions avoid line-order dependence (conditions print in completion order):
- 06-BinarySearch: all 20 conditions ok, exit 0.
- 10-SITA: commonEntry/swap proven through their invariants; the rearranging
  driver is excluded from the report (its contract is weaker than the default
  runtime-check policy — the in-process suite also only asserts commonEntry/swap).
- 08-Java5: the \sum binder is rejected loudly with the structured
  UnsupportedFeatureException and a non-zero exit, not silently mis-encoded.
- 05-ReverseArray: UNBOUNDED+invariant (the U_INV config) — generation succeeds,
  both methods are reported (31 conditions total, generation-pinned by the
  goldens), reverse() itself never yields a counterexample; its quantified \old
  queries exceed the solver budget and are reported UNKNOWN (the in-process suite
  treats this fixture as generation-only and asserts no verdicts).
Bounded single-variable \sum ranges with a direct array-read body are
translated to a recursive `jmlSum` function; other shapes abort with a
structured UnsupportedFeatureException. Array-length terms of constant
symbols in BOUNDED \sum queries fold into shared implicit `$len_*`
constants (explicit declarations stall Z3's rec-fun instantiation on the
invariant/variant checks), and every solver-answer walker skips the
interleaved "unknown constant" warnings. \index and \strictly_nothing
are supported; the KeyFor (08-Java5) fixture and the SITA3 search/swap
examples verify in both modes.
08-Java5's \sum contract now verifies in the real CLI subprocess in both
bounded and --unbounded mode (11 conditions, 0 failing — the fixture gains
the explicit `requires a != null` the VCG needs) instead of being rejected.
New shell-test fixtures cover BinarySearch, the \sum example, SITA (with
the weak rearrange contract excluded) and ReverseArray's honest UNKNOWN/FAIL
report; shelltester gains a `sortOutput` mode that compares sorted
normalized lines so solver-completion order cannot fail the tests.
Resolves the SmtTermFactory.list() conflict (main's #83 made the same
listOf()-dropping fix; take main's version without the stale TODO).
Records the VCG \sum work (e8c120a, d0ee304) in jmltk-changelog.md
per the #87 changelog convention.
…(P3.2)

\forall/\exists/\sum with an element or range domain (e.g.
'(\forall int e : a; e > 0)', stored on JmlQuantifiedVariable per the
merged core #83) have no VCG desugaring: the old path bound the variable
by its declared type and translated the body, silently *dropping the
domain* — '(forall ((e Int)) (> e 0))' — an unsound encoding (the
requires collapses to false assumptions). checkSupported now aborts such
quantifiers with a structured UnsupportedFeatureException naming the
binding form, matching the existing loud-rejection policy.

VcgSemanticsTest: two new unsupportedConstructs rows
(quantIterableDomain, sumIterableDomain); suite 333->335, all green
(vcg 873, cli 4).
The pinned setup-smt action installed Z3 4.13.3 on CI while local development
uses 4.16.0. Z3 < 4.15 cannot terminate on quantified bit-vector obligations
such as 06-BinarySearch's postcondition (the \exists over array indices under
the nested sortedness \forall): the negated obligation runs until the solver
watchdog cancels it, mapping the two postcondition conditions to UNKNOWN
(`????`) and failing the new binarySearchIsVerifiedThroughItsLoopInvariant
e2e test and the search.txt shell fixture. Verified: identical failure on the
pre-merge tip (the merge with main and the element-domain guard are not
involved), reproducible locally with 4.13.3; 4.15.4 decides the query instantly
and the full :tools:smt/:tools:vcg/:tools:cli suites plus all shell tests pass.

4.15.4 is the newest Z3 the pinned action installs on every matrix OS (linux
glibc 2.39, macos 13.7.6, windows): 4.16.0's macos asset (osx-15.7.3) needs a
newer action, so 4.15.4 is the max compatible pin.
The Windows start script must use the %APP_HOME%\\lib\\* classpath wildcard:
cmd.exe expands %APP_HOME% when 'set CLASSPATH=' executes, so an explicit jar
list becomes an absolute-path classpath of >8k chars for 115 jars and blows
the 8191-char line limit on every continuation line ('The input line is too
long' — a chunked CLASSPATH variant fails exactly this way).

Java expands that wildcard in directory-enumeration order (first jar to offer
a class wins), so the distribution must not contain two com.github.javaparser
implementations. The LSP's key.core transitively pulls in
org.key-project.proofjava:javaparser-core(-serialization/symbol-solver-core)
3.28.0-K13.6, whose ParserConfiguration lacks setProcessJml; on Windows it
sorts before the fork core and 'jmltk vcg' died with NoSuchMethodError (Linux
start scripts list jars in dependency order and were unaffected; main's
VcgCommand predates the setProcessJml call, which is why main's Windows shell
tests were green).

Exclude the proofjava group from the :tools:lsp dependency so the
distribution ships exactly the fork core. Verified: proofjava-first classpath
reproduces the CI stack trace; the deduplicated lib\* wildcard proves all 20
binarySearch conditions; shell tests (6/6), :tools:cli:test and
:tools:lsp:test pass; spotless clean. Changelog updated.
The sita.txt shell fixture passes the --vcg-exclude glob inside single
quotes, which group on POSIX sh but are literal characters on cmd.exe: on
Windows the CLI received the pattern 'KeySITA3#rearrange()*' including the
quote characters, matched nothing, and the weak rearrange() contract was
reported (33 conditions, 10 failing) instead of being excluded (20, 0
failing). Double quotes group on both shells and cmd.exe does not glob, so
the literal pattern reaches the CLI unchanged on every OS.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants