Skip to content

VCG by AI - #69

Draft
wadoon wants to merge 15 commits into
mainfrom
weigl/vcg
Draft

wadoon wants to merge 15 commits into
mainfrom
weigl/vcg

Conversation

@wadoon

@wadoon wadoon commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

No description provided.

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.
@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

The overall line coverage in commit 34d6f8e in the weigl/vcg branch is 58%. The line coverage in commit 471cb6c in the main branch is 57%.

Show a line coverage summary of the most impacted files.
File main 471cb6c weigl/vcg 34d6f8e +/-
com/github/java...oneVisitor.java 26% 33% +7%
io/github/jmlto...tTermFactory.kt 43% 82% +39%
io/github/jmlto.../SExprParser.kt 0% 60% +60%
io/github/jmlto...cg/VcgFacade.kt 0% 85% +85%
io/github/jmlto...olver/Solver.kt 0% 90% +90%
io/github/jmlto...VerifyMethod.kt 0% 91% +91%
io/github/jmlto...lkit/vcg/Vcg.kt 0% 92% +92%
io/github/jmlto...prTranslator.kt 0% 99% +99%
io/github/jmlto...g/Normalizer.kt 0% 99% +99%
io/github/jmlto...cg/VcgResult.kt 0% 100% +100%

Updated October 04, 2026 12:09 UTC

@wadoon wadoon self-assigned this Sep 27, 2026
wadoon added 7 commits October 3, 2026 12:51
… 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.

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.

1 participant