Skip to content

Use new support for invariants - #428

Draft
jcp19 wants to merge 8 commits into
masterfrom
claude/gobra-syntax-update-pr412-fhghc8
Draft

Use new support for invariants#428
jcp19 wants to merge 8 commits into
masterfrom
claude/gobra-syntax-update-pr412-fhghc8

Conversation

@jcp19

@jcp19 jcp19 commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Supersedes #412

jcp19 and others added 2 commits January 25, 2026 19:50
Resolves conflicts by keeping the native-invariants approach
(Invariant/EstablishInvariant/critical blocks) while adopting the new
predicate expression instance syntax SharedInv{...} introduced on master
(#416). Also migrates the new processEPIC (#261) from the ghost mutex to
invariants and removes the now-unused ghost_sync package.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012eBw3LWL6qAVMS4aLVW5ks
@jcp19
jcp19 force-pushed the claude/gobra-syntax-update-pr412-fhghc8 branch from 478cabf to a923d62 Compare August 6, 2026 12:17
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RtaS6t2NWpe3WRn6w9gXz4
Gobra does not allow interface methods to be marked as `atomic`, so calls to
`BatchConn.ReadBatch` and `BatchConn.WriteBatch` may not occur inside a critical
region. As a consequence, the caller can no longer perform the transitions of
the IO specification atomically with the physical read/write.

Instead, we now assume that both methods open the shared invariant themselves
and perform the corresponding transitions internally, atomically with the
respective physical operation. Their contracts take `Invariant(SharedInv{...})`
instead of the `io.token` and, in exchange:
- `ReadBatch` returns the sequence of abstract values of the packets that were
  received together with a witness for each of them, which makes the prophecy
  over the number of received packets unnecessary;
- `WriteBatch` requires the witness that the packet being sent is in the output
  buffer of the model, which is what is needed to establish the guard of the
  `send` transition.

The `MultiReadBio` definitions are kept, as they specify the effect that
`ReadBatch` is assumed to have on the shared invariant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RtaS6t2NWpe3WRn6w9gXz4
Comment thread router/dataplane.go
Comment on lines -976 to -988
// Multi recv event
// @ ghost ioLock.Lock()
// @ unfold SharedInv{dp, ioSharedArg}()
// @ ghost t, s := *ioSharedArg.Place, *ioSharedArg.State
// @ ghost numberOfReceivedPacketsProphecy := AllocProphecy()
// @ ExtractMultiReadBio(dp, t, numberOfReceivedPacketsProphecy, s)
// @ MultiUpdateElemWitness(t, numberOfReceivedPacketsProphecy, ioIngressID, s, ioSharedArg)
// @ ghost ioValSeq := MultiReadBioIO_val(t, numberOfReceivedPacketsProphecy)

// @ ghost sN := MultiReadBioUpd(t, numberOfReceivedPacketsProphecy, s)
// @ ghost tN := MultiReadBioNext(t, numberOfReceivedPacketsProphecy)
// @ assert dp.dp3s_iospec_ordered(sN, tN)
// @ BeforeReadBatch:

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extract this into a lemma that shows that one can always extract the permission to receive various packets from the IO permissions

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in ec283f6, as ExtractRecvPermissions in router/dataplane_concurrency_model.gobra.

Note that the block this comment points at no longer exists at the call site: since interface methods cannot be marked atomic, ReadBatch is now assumed to open the shared invariant itself and perform the recv transitions internally, so the critical region around it is gone. I extracted the ghost code into the lemma anyway, because it states exactly the proof obligation underlying that assumption — from the contents of SharedInv{dp, y} for a place t and state s, and for any n, it yields MultiReadBio(t, n), the resulting sN/tN with dp3s_iospec_ordered(sN, tN), the abstract values of the received packets, and a witness for each of them.

The one thing it cannot discharge is advancing the IO token from t to tN, which is the physical reception of the packets; that remains the assumption in ReadBatch's contract. For the same reason the lemma takes the unfolded contents of the invariant rather than Invariant(SharedInv{dp, y}): it cannot close the invariant again, as the token is still at t.

So the lemma is currently a justification rather than something the router applies. If you would rather not carry a lemma with no call site, the alternative is to drop it together with MultiReadBio and friends, which nothing else uses any more — happy to do that instead.


Generated by Claude Code

`ExtractRecvPermissions` shows that the permission to receive an arbitrary
number of packets can always be extracted from the IO permissions held by the
shared invariant, together with the resulting place and local state and a
witness for every received packet.

This is the ghost code that used to be inlined in the critical region around
`ReadBatch`. It now states the proof obligation underlying the IO specification
assumed for `BatchConn.ReadBatch`: everything that `ReadBatch` is assumed to do
to the shared invariant follows from the invariant alone, except for advancing
the IO token, which is the physical reception of the packets.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RtaS6t2NWpe3WRn6w9gXz4
Conflicts came from #433, which made most of the IO-spec identifiers exported.
Resolved by keeping the invariant-based specifications and renaming their
identifiers accordingly (io.token -> io.IOToken, dp3s_* -> Dp3s_*,
absIO_val -> AbsIO_val, path.ifsToIO_ifs -> path.IfsToIO_ifs, s.ibuf/s.obuf ->
s.Ibuf/s.Obuf). verification/utils/ghost_sync stays deleted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RtaS6t2NWpe3WRn6w9gXz4
The OOB buffers of both the read and the write messages are nil, so all of
their Mem() instances and writeMsgInv share the single predicate instance
sl.Bytes(nil, 0, 0). After merging master, Gobra stopped being able to add up
the shares of that instance held at the end of the loop body and reported that
the permission for the fold might not suffice, even though the same fold
verifies on both parents of the merge.

Provide a fresh instance with sl.NilAcc_Bytes() before the fold, as is already
done earlier in the same block. This is sound: the body of sl.Bytes is vacuous
for an empty slice, so the instance protects no memory.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RtaS6t2NWpe3WRn6w9gXz4
After merging master, the verification of Run keeps failing on a single
permission check, but not always on the same one: first on the fold of
writeMsgInv at the end of the loop body, and then, once an extra share of
sl.Bytes(nil, 0, 0) was made available there, on the sl.Bytes(ubuf, ...)
conjunct of the loop invariant. Making more permission available cannot make a
permission check fail, so neither of these is a genuine shortfall; they are
assertions that exceed the 45s assert timeout, and which one tips over varies
with unrelated perturbations of the proof.

Both parents of the merge verify, so raise the timeout for this package rather
than weakening any specification. Only assertions that would otherwise be
reported as failures are affected, so the effect on the running time of the job
is bounded by a couple of minutes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RtaS6t2NWpe3WRn6w9gXz4

jcp19 commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator Author

verify-router has been failing since the merge of master, on a single permission check that moves around. Flagging the diagnosis because the fix I pushed touches router/gobra.json, not a specification.

What happened

head verify-router error
ec283f6 (this PR, before the merge) ✅ 68 min
1c4bae8 (master, #433) ✅ 80 min
93d309f (the merge) ❌ 78 min dataplane.go:1205 fold writeMsgInv(writeMsgs): permission to sl.Bytes(writeMsgs[0].OOB, 0, len(...)) might not suffice
66f99a7 (+ an extra sl.NilAcc_Bytes() before that fold) ❌ 96 min dataplane.go:1055 loop invariant not preserved: permission to acc(sl.Bytes(ubuf, 0, len(ubuf)), writePerm) might not suffice

Both parents of the merge verify, and the merge is a mechanical resolution: at every conflict I kept the invariant-based side and applied #433's renames, so the code at both failing points is master's modulo ioLock removal.

The second data point is what settles it. Making more permission available cannot make a permission check fail, so the first error was not a genuine shortfall either — these are assertions exceeding the 45s assert_timeout, and which one tips over shifts with unrelated perturbations of the proof. Consistent with that: the failing conjuncts are exactly the ones needing several symbolic shares of sl.Bytes(nil, 0, 0) to be added up (the OOB buffers of msgs and writeMsgs are nil, so all their Mem() instances and writeMsgInv share that one predicate instance), and the run time went up rather than the proof going wrong.

What I pushed (aff08d3): "assert_timeout": 120000 in router/gobra.json, plus the sl.NilAcc_Bytes() before the fold from the previous commit. Only assertions that would otherwise be reported as failures are affected, so the cost should be a couple of minutes. If the timeout alone is enough, the sl.NilAcc_Bytes() line is redundant and I am happy to drop it — say the word.

Worth noting that this looks like it was already close to the edge on master, so it may flake there too. If you would rather tune chop than the timeout, or you can reproduce locally and see something I cannot, tell me and I will redo it that way. I have no way to run Gobra in this environment — the egress policy here blocks repo.scala-sbt.org and repo.typesafe.com (so sbt cannot resolve Gobra's build plugins) and the ghcr.io blob host for the prebuilt image — so everything above is inferred from CI alone.


Generated by Claude Code

jcp19 commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator Author

Green on aff08d3verify-router passed in 88 min, in line with the 68–80 min of the two parents, so raising the timeout cost roughly nothing. That confirms the diagnosis above: neither failure was a real permission shortfall.

The offer stands to drop the sl.NilAcc_Bytes() line if you would rather have the timeout alone carry it.


Generated by Claude Code

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