Skip to content

Predicates as a combinator, the final stretch#391

Open
giltho wants to merge 19 commits into
extract-preds-6from
extract-preds-7
Open

Predicates as a combinator, the final stretch#391
giltho wants to merge 19 commits into
extract-preds-6from
extract-preds-7

Conversation

@giltho

@giltho giltho commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Based on #390 and #388
Sorry i'm giving up on spliiting into PRs it's taking me too much time. I'm still splitting into adequate commits.

The predicate combinator is still, effectively, in the making, but I've had to do the following refactors so far (in addition to the big refactors in #380 #381 #382 #385 #386 #387 #388 and #390):

  • Make everything "MonadicSMemory" (since the predicate combinator is, it needs its inputs to be). This means rewriting JS to use immutable memory and monadic from scratch.
  • make wisl immutable. I'm pretty sure it's unsound otherwise. In fact, I removed all notions of immutability in memory models.
  • remove all dead code after this cleanup

@giltho giltho changed the title Predicates as a combinator, the final line Predicates as a combinator, the final stretch Jul 9, 2026
giltho added 13 commits July 9, 2026 15:22
… fixes, of_resolver, single verifier stack

Behavior-neutral groundwork for moving predicate/wand reasoning into the
Abstraction combinator:

- Combinators.Abstraction.err_t becomes a proper variant (MissingUPred/
  MissingWand/AsrtFailure/OtherErr/SubError) with can_fix/get_fixes/
  get_recovery_tactic/get_failing_constraint mirroring the StateErr
  behaviour PState/Matcher exhibit today for the same situations.
- Fix the split_further stub: SubPred splitting now delegates to the
  underlying memory (it was silently disabled since the wiring commit,
  turning off core-pred splitting in wand packaging).
- Fix sure_is_nonempty to account for non-pure predicates and wands.
- Delayed.of_resolver: inverse of resolve, entry point for direct-style
  (multi-pc) matching code in memory models.
- Drop S_verification_NEEDS_CLEANUP: the symbolic debugger now uses the
  same Verification stack; V_lifter additionally re-projects memory
  errors (SubError) onto the tool lifter.
- Dormant Config.preds_in_memory transition flag; demote a stray stdout
  print in Matcher to verbose logging.

Validated: dune build @ALL, dune fmt, dune test, wisl quicktests,
Amazon C + JS (all specs), act sll.c (15/0/2), C + JS env suites.
Matching_walker holds the representation-generic matching-plan machinery
(produce_assertion/produce/produce_posts, match_ins_outs_lists,
match_assertion, match_mp, match_ + its fuel-limited recovery loop),
polymorphic over the state and error types via an ops record, in direct
style (explicit states and result lists).

Matcher.Make becomes the state-level instantiation: state operations map
to State.*, and the user-predicate/wand arms of produce/consume are
passed as transitional hooks carrying the exact legacy Pred_state-based
code. The fold/unfold/package machinery stays in Matcher for now and
goes through the walker for assertion-level production and matching.
match_kind/recovery_tactic move to the walker with manifest re-exports,
so all external users (PState, Verifier, match_map, debuggers) are
unaffected.

This is a pure factoring: no behavior change intended, and none
observed.

Validated: dune build @ALL, dune fmt, dune test, wisl quicktests,
Amazon C (114s, all specs) + JS (all specs), act sll.c (15/0/2),
C + JS env suites.
…ombinator

Implements the full predicate/wand machinery inside
Combinators.Abstraction, in direct style over an explicit matching state
(memory + path condition), entering Delayed only at the MonadicSMemory
boundary (via Delayed.of_resolver):

- the memory-level instantiation of the matching walker (pure/typing ops
  mirror SState.assume_a/assert_a/assume_t/get_type/simplify_val over
  the materialized pc; sub-memory consume/produce run under the same
  per-operation matching-flag policy as SState);
- consume with candidate lookup (reusing Preds.consume_pred and
  Wands.consume_wand with pc-based equality) and auto-folding on miss;
- fold/unfold (incl. guards and additional bindings), rec_unfold,
  unfold_all, the predicate selection strategies, unfold_with_vals,
  unfold_concrete_preds, try_recovering — ports of the Matcher
  algorithms over the immutable abs state;
- wand packaging with two independent path conditions (lhs vs current);
- internalized automation, replacing engine-driven recovery: sub-action
  failures retry after fold/unfold recovery (mirroring the interpreter
  loop, fuel 10), consumption failures recover step-locally, and
  concrete-ins predicates are eagerly unfolded after every boundary
  operation (verification mode only; UX errors flow up unswallowed for
  bi-abduction);
- the four predicate actions (fold/unfold/gunfold/package) implemented
  against the SLCmd encoding.

The new code paths are cold until Config.preds_in_memory routes
predicate traffic below the state: pre-flip, the (live) recovery and
unfold-concrete wrappers are no-ops over empty predicate sets.

Validated: dune build @ALL, dune fmt, dune test, wisl quicktests,
Amazon C + JS (all specs), act sll.c (15/0/2), C + JS env suites.
…n_memory

All seams are now in place, off by default (toggle with
GILLIAN_PREDS_IN_MEMORY=1):

- Matcher's legacy user-pred/wand produce/consume arms fall through to
  the generic core-predicate path (-> the memory) when the flag is set;
- PState.execute_action delegates the four predicate actions to the
  memory, keeping only the state-level concerns as a shim (argument
  evaluation through the store, Unfold binding spec-vars, the legacy
  post-action simplifications);
- the Normaliser routes user-pred/wand core predicates through
  produce_core_asrts instead of building Preds.t/Wands.t directly;
- a new reserved action GILLIAN_USER_PRED__recover lets the state-level
  automation (assume-with-unfold on guarded gotos, the matching retry
  loop) delegate fold/unfold recovery to the memory: this was needed in
  practice (WISL pointer arithmetic relies on assume-unfold killing
  junk-pointer branches, and post-conditions rely on the retry loop for
  pure facts hidden under folded predicates);
- the debugger lifter re-projects the memory's predicates so the
  predicate pane keeps working.

Fixes surfaced by A/B-running the suites both ways:
- PState's *-assert/consume forget mechanism dropped the substituted
  state (correct only under in-place mutation; the immutable memory
  made the predicate rename vanish) - now binds the substituted state;
- the walker's matching retry loop only progressed because
  try_recovering mutated the state-level predicate set in place; an
  advance_recovery_base ops field lets immutable-store instantiations
  progress by recovering from the previously recovered state;
- in-memory recovery results are simplified at the state level
  (collapsing the equality chains an unfold introduces), mirroring the
  legacy unfold;
- is_overlapping_asrt no longer forwards user-pred/wand names to the
  underlying memory (some memories fail hard on unknown names);
- pcs that learn a literal false are pruned at the memory boundaries
  (the legacy engine dropped such branches at its satisfiability
  checks);
- no recovery/eager-unfolding around plain consume/produce: recovery
  belongs to the matching retry loop and the action path, like the
  legacy engine - anything more stacks search on search and blows up
  on recursive predicates.

Status with the flag ON: wisl quicktests (127/127 incl. wand.wisl),
dune test, act sll.c (15/0/2), C and JS env suites all pass; Amazon C
is at 34/38 procs (4 under investigation), Amazon JS under test.
Flag OFF (default): the full validation matrix passes unchanged.

Validated (flag off): dune build @ALL, dune fmt, dune test, wisl
quicktests, Amazon C + JS (all specs), act sll.c (15/0/2), C + JS env
suites.
With GILLIAN_PREDS_IN_MEMORY=1, the complete validation matrix now
passes: wisl quicktests (127/127, incl. wand.wisl designed failures),
dune test, act sll.c (15/0/2), C and JS env suites, Amazon C (all 38
specs) and Amazon JS (all specs).

Final two fixes:
- unfoldings must be simplified with ~matching:true before any
  matching:false reduction sees them: an unfolding can pin a location
  lvar through a pure fact while the sub-memory produce, unable to see
  the equality, mints a fresh abstract location for the same lvar; only
  an aloc-unifying (matching) simplification reconciles the two.
  The PState action shim now applies it after Unfold and GUnfold, and
  the memory-level [learn] splits conjunctions (like Pc.extend) so that
  location resolution sees equalities buried in reduced pair-equalities
  in the first place.
- candidate selection for user predicates and wands is outs-aware
  again: the walker forwards the outs it already knows through a
  transitional ambient channel (Consume_hints, same dynamic-scoping
  idea as MP.get_pred_defs), and the memory uses them for scoring only.
  Without this, two same-name predicates sharing their ins (e.g. two
  ListToSet over the same list) are picked arbitrarily where the legacy
  consume picked by outs, failing the post-hoc outs check.

Known follow-up before flipping the default: Amazon JS is functionally
correct but ~40x slower flag-on (Amazon C: +20%); the hot suspects are
per-call pfs materialization in the memory-level equality checks and
the eager concrete-unfold pass on every action.
Behavior-parity + perf groundwork for GILLIAN_PREDS_IN_MEMORY=1; flag
off is unchanged. Matrix green both ways (quicktests 127, dune test,
act 15/0/2, Amazon C all specs, C/JS env suites). Amazon JS passes
flag-on but ~40x slower; investigation notes live in the plan file.

- Walker recovery-loop progress now uses an explicit exclusion set
  instead of relying on Preds mutation (or the interim
  advance_recovery_base, which made recovery dig through recursive
  preds): ops.try_recovering takes/returns ~tried candidates and always
  re-launches from astate_i; Abstraction's consume_pred_with_vs
  ~exclude filters tried candidates while popping the chosen one from
  the full pred list. The _recover action carries tried as a 4th
  encoded arg and returns the chosen candidate in its ret values.
- execute_sub_action recovers once per action evaluation instead of
  once per error branch.
- Pc: memoize the materialized pfs/gamma (hot in candidate scoring);
  invalidated on copy/make/extend/extend_types and Abstraction.learn.
- Consume_hints: ambient outs-hint channel so the predicate-carrying
  memory scores candidates on known outs (selection only; outs are
  still checked by the walker). Fixes wrong-candidate picks in
  Amazon C (ListToSetUnion, enc_ctx_deserialize).
- Abstraction.learn splits conjunctions (location resolution only
  scans top-level formulas) and resets the pc memo.
- should_unfold is purely syntactic (Expr.to_literal on in-args).
Fixes the ~40x Amazon JS slowdown with GILLIAN_PREDS_IN_MEMORY=1
(391s -> 9.7s, on par with the legacy 9.5s).

Abstraction.execute_sub_action recovered internally on fixable
sub-memory action failures, which preempted the interpreter's own
action-retry loop (the error never reached it). The internal path has
no state-level simplify, so the alias chains introduced by each unfold
were never canonicalized and every later action on the same structure
re-dug through it: readElements fired 440 GetCell recovery chains
(CElement unfolded 154x, ArrayBuffer 106x) and ~5x the SMT queries,
against 84 recoveries total for the legacy engine.

Sub-action failures now flow up unswallowed; the interpreter retries
through PState.try_recovering, which flag-on delegates to the reserved
recover action and simplifies the recovered states — the exact legacy
cadence. (Flag-off this recovery was always a no-op: the combinator's
pred list is empty when predicates live outside the memory.)

Full matrix green both ways: dune test, wisl quicktests 127/127,
Amazon C all specs (123s on / 112s off), Amazon JS all specs
(9.7s / 9.5s), act sll.c exactly 15/0/2, C env (output identical
modulo lvar numbering) and JS env suites.
User-defined predicates and wands are now handled by the
Combinators.Abstraction memory combinator by default; the legacy
PState/Matcher path remains selectable with GILLIAN_PREDS_IN_MEMORY=0
until the cleanup phase removes it.

Full matrix green with the new default: dune test (transformers + C
incremental now exercise the in-memory path), wisl quicktests 127/127
(and 127/127 with =0), Amazon C all specs (123s), Amazon JS all specs
(9.5s), act sll.c exactly 15/0/2, C env (fail-lines identical to the
validated run) and JS env suites.
Predicates and wands are now handled exclusively by the
Combinators.Abstraction memory combinator; the transition flag
(Config.preds_in_memory / GILLIAN_PREDS_IN_MEMORY) is gone.

- matching_walker: drop the transitional consume/produce hooks; user
  predicates and wands reach consume_core_pred/produce_core_pred like
  any core predicate.
- Matcher: delete the legacy arms and the moved algorithms
  (consume_pred, consume_wand, fold, unfold, unfold_with_vals',
  rec_unfold, unfold_all, fold_guarded_with_vals,
  select_guarded_predicate_to_fold, Predicate_selection_strategies,
  consume_pred_with_vs, extend_subst_with_bindings, Wand_packaging,
  package_wand, unfold_concrete_preds and the legacy try_recovering
  body). S shrinks to Logging, produce, produce_assertion,
  produce_posts, try_recovering, match_assertion and match_.
  simplify_astate stays (recovery simplifies recovered states).
- PState: delete eval_pred_slcmd (actions always go through the
  eval/spec-vars/post-simplify shim), the legacy assume-unfold branch,
  run_spec_aux's state-level concrete unfolding (the memory's post-pass
  covers it) and set_preds/set_wands.
- Normaliser: separate_assertion returns core/pure/types only;
  normalise_preds/normalise_wands deleted (predicate facts are learned
  by the memory when producing).
- command_line: the debugger predicate pane always re-projects the
  memory's predicates.

Net -1691 lines. Full matrix green: dune test, wisl quicktests
127/127, Amazon C all specs (123s), Amazon JS all specs (9.5s), act
sll.c exactly 15/0/2, C env (fail-lines identical) and JS env suites.
Predicates and wands live in the memory (Combinators.Abstraction), so the
state-level {state; preds; wands} record carried permanently-empty fields.
This deletes Pred_state and makes Matcher.t and PState.t the underlying
SState directly.

To keep the dependency graph acyclic once SState itself instantiates the
matcher (next commit), Matcher.Make's parameter is narrowed from SState.S
to a self-contained MatchableState signature spelling out exactly what the
matching engine needs from a state.

The matching reports' astate payload shrinks accordingly (AstateRec is now
just the state); the debugger's predicate pane is unaffected since it is
re-projected from the memory by the command-line lifter.

Validation: dune test; wisl quicktests 127; Amazon C all specs (122.8s);
Amazon JS all specs (9.46s); act sll.c 15 success / 0 error / 2 bug;
C env suite (designed array_oob failure + pre-existing SMT encode warnings
only); JS env suite clean.
…hing

Verification runs directly on SState built over the predicate-carrying
memory (Combinators.Abstraction); the PState layer had nothing left to add.
SState now implements the State.S members that previously raised in pure
symbolic execution — evaluate_slcmd, run_spec/run_par_spec, match_invariant,
frame_on, produce/produce_posts/match_assertion, try_recovering — through an
internal Matcher.Make instantiation (made possible by the MatchableState
narrowing), and gains the pred-action shim on execute_action and the
unfold-on-assume automation. The latter is gated on verification or
bi-abduction mode so plain wpst memories never receive the reserved recover
action; in symbolic mode there are no predicates to unfold anyway.

Consumers are rewired to plain SState.S: Verifier takes a single state
parameter (and owns the postcondition `matches` check), Normaliser, BiState
and Abductor retype their functors, and the transformers bi-abduction runner
drops its PState wrappers. Gillian.Symbolic.PState is gone; Verifier.S
renames SPState to State (and drops its redundant SState alias).

Validation: dune test; wisl quicktests 127; Amazon C all specs (122.3s);
Amazon JS all specs (9.49s); act sll.c 15 success / 0 error / 2 bug;
C env suite (designed array_oob failure + pre-existing SMT encode warnings
only); JS env suite clean.
… move to the interpreter

Fold/Unfold/GUnfold/Package are executed by the (Abstraction-wrapped)
memory, but three concerns around them need the store, the spec-var set,
or state-wide simplification: evaluating the store-dependent
sub-expressions, registering Unfold binders as spec-vars, and the
post-action simplification. These lived in a predicate-aware shim on
SState.execute_action (exec_pred_action_in_memory + a shadowing
execute_action that decoded SLCmds via SLCmd.of_action) — making the
state aware of predicates, which it should not be.

Move all three into the interpreter's SL-command branch, which already
decodes the command via SLCmd.to_action. The shim is deleted;
SState.execute_action is now purely the core memory action executor and
the state is predicate-unaware. No interface changes — State.S already
exposes eval_expr/add_spec_vars/simplify.

Evaluation is done on the structured SLCmd via SLCmd.map before
to_action, so each argument is re-encoded into a fresh EList (never
decayed into a literal list that SLCmd.of_action could not decode);
Package arguments stay raw and Unfold/GUnfold keep the ~matching:true
simplification, exactly as the shim did.

Validation matrix all green (identical to baseline): dune test;
quicktests 127; Amazon C all specs (124s); Amazon JS all specs (9.7s);
act sll.c 15/0/2; C env (designed array_oob failure + pre-existing SMT
Set warnings only); JS env clean.
@giltho

giltho commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

I honestly don't know how you go at reviewing this
Well, commit-by-commit for sure, but this is massive....

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