Fix namespace collision and SMACK assert encoding in BoogieToStrata#1149
Fix namespace collision and SMACK assert encoding in BoogieToStrata#1149PROgram52bc wants to merge 21 commits into
Conversation
1. Namespace collision (issue 1 in #1148): Constants that share a name with a procedure are now prefixed with __const_ in the Strata output. The rename is applied consistently in declarations and references. 2. Recursive synonym resolution (Fixes issue 2 in #1148): DealiasTypeExpr now recurses on its result, resolving synonym chains like ref := i64 := int fully to the base type. Previously it resolved only one level, causing panics on comparison/arithmetic operators applied to multi-level type synonyms. 3. SMACK assert encoding (issue 3 in #1148): Procedures named assert_.* now get a requires (arg != 0) precondition emitted. Call elimination generates VCs at each call site, making SMACK assertions verifiable. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the separate _constantRenames/_procedureNames approach with a unified _renames dictionary populated by scanning all declarations upfront. Procedures are claimed first; constants, functions, and globals that collide get prefixed with their kind (__const_, __func_, __var_). Also restores InferModifies and Prune options in BoogieToStrata.cs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Prune=true caused Boogie to remove declarations needed by axioms and quantifiers, shifting line numbers and breaking verification for tests like BooleanQuantification, Axioms, TypeSynonyms2, Quantifiers, and Lambda. InferModifies is retained as it's needed for SMACK support. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
tautschnig
left a comment
There was a problem hiding this comment.
Thanks for tackling all three #1148 issues in one PR — I ran the three fixes end-to-end on the included reproducers and each of them does what the description claims. The direction looks right, and I especially like that fix (3) follows atomb's suggestion of an opaque procedure with a precondition rather than inlining assert. I have a mix of blocking and non-blocking comments, grouped below:
Blocking
-
Test harness gap.
VerifyTestFileadds--checkwhen no.expectfile exists, which literally emitsSkipping verification.without invoking the solver. I re-ran the three new tests: with no.expect,SmackAssert.bplpasses CI even though the whole point of the test is thatcall $r := assert_.i32(0);should now fail verification. Please add.expectfiles for all three new tests (SmackAssert.expectcrucially must contain afailedline so the harness flipsexpectedExitCodeto 2 and actually catches regressions). -
Fix (2) has no regression test.
dealiasTypeExprnow recurses, but there is no.bpltest exercising a multi-level synonym chain (the issue's reproducerTypeSynonymComparison.core.stwasn't carried over). ExistingTypeSynonyms{1,2}.bplonly use single-level synonyms. Without a test, this fix has no CI coverage. A 10-line.bplusingtype i64 = int; type ref = i64;with<=/+overrefwould close this. -
Rename mechanism is not applied consistently. Four of the places that emit variable names still call
Name(...)instead ofNameOf(decl, ...). This is only triggered when a global gets renamed (i.e. its sanitized name collides with a proc/const/func), but once it does, the output is broken. Reproducer:const $a.b: int; axiom $a.b > 0; var $a_b: int; // both sanitize to _a_b procedure main() returns (r: int) modifies $a_b; { $a_b := 1; havoc $a_b; r := $a.b + $a_b; }
Produces (inout parameter name and havoc target still use the pre-rename identifier, while the identifier expression uses the renamed one):
procedure main(inout _a_b : int, out r : int) { _a_b := 1; havoc _a_b; r := (_a_b + __var__a_b); // __var__a_b is undefined }...and
strata verifyreportsUnknown expr identifier __var__a_b. See the inline comment atVisitCallCmdfor the full list of affected sites. -
StartsWith("assert_")is too permissive and triggers on user-written procedures. Reproducer:procedure assert_helper(p: int) returns (r: int); procedure main() returns (r: int) { call r := assert_helper(0); }
emits
requires (p != 0);onassert_helpereven though the user never wrote it. Any Boogie program with a procedure whose name happens to start withassert_(e.g.assert_helper,assert_invariant,assert_equal) would start failing verification. SMACK's actual pattern isassert_.<type>(literal dot); please tighten to match that specifically.
Non-blocking
InferModifies = trueisn't mentioned in the PR body. It's not just a knob — it runsModSetCollector(mutating the program) and, viaCheckModifies, suppresses modifies-clause typechecking. Please document the motivation and add a.bpltest with a procedure that mutates a global without an explicitmodifiesclause, so regressions in this new assumption are caught.- Proof opportunities for
dealiasTypeExpr. Since this ispartial def, strong termination isn't provable without an acyclicity argument onp.globalContext. Natural lemmas that the rest of the file could rely on:dealiasTypeExpr_idempotent : dealiasTypeExpr p (dealiasTypeExpr p te) = dealiasTypeExpr p te, and a "result is fully unwound" lemma: ifdealiasTypeExpr p teis.fvar _ idx #[], thenp.globalContext.kindOf! idxis neither.expr _nor.type [] (.some _). If you don't want to prove these, at least consider a Lean-side#guardtest constructing a three-level synonymProgramand checking the result is.int. - Refactoring the rename builder. The
ClaimOrRenamelogic would be more readable as a named static helper that takes theclaimedset and the_renamesdict as parameters, rather than a closure insideEmitProgramAsStrata. The ordering "procs → impls → consts → funcs → globals" is a deliberate tie-breaker (first-seen wins) — please document it at the_renamesfield, since it's a semantic choice with implications (e.g. a name collision between a proc and a const always renames the const, never the proc). - Duplicated spec emission for
assert_.*. The newspec { requires ... }block inVisitProcedureduplicates the emission pattern inWriteProcedureHeader. Consider injecting a syntheticRequiresintonode.Requiresjust before callingWriteProcedureHeaderso the existing code path handles it uniformly. IdentifierExprfallback.identExpr.Decl != null ? NameOf(...) : Name(...): post-Boogie-resolution,Declshould always be non-null. Silently falling back toName(...)emits the unrenamed name and is wrong in the collision case. Consider asserting non-null instead, or at least add a comment about whenDeclcan be null and why the fallback is safe.
1. Rename mechanism: use NameOf(decl, ...) instead of Name(...) at all
sites that emit variable names (EmitSimpleAssign, VisitCallCmd,
VisitHavocCmd, VisitGlobalVariable, WriteFormals) so that renamed
globals are referenced consistently throughout the output.
2. SMACK assert pattern: tighten StartsWith("assert_") to
StartsWith("assert_.") to match only SMACK's actual encoding
(assert_.i32, assert_.i64) and avoid false positives on user
procedures like assert_helper.
3. Regression tests: add .expect files for SmackAssert,
NamespaceCollision, SanitizationCollision (fixing the test harness
gap where --check mode skipped verification), plus new test cases
for TypeSynonymChain, GlobalVarRenameCollision, and
AssertPrefixFalsePositive.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Problem:
When an assert_.<type> procedure had existing ensures (or requires)
clauses, the generated Strata output contained two separate spec {}
blocks — one from WriteProcedureHeader (for existing specs) and a
second from VisitProcedure (for the synthetic requires). This produced
invalid output like: `spec { ensures ...; }spec { requires ...; }`.
Root Cause:
VisitProcedure emitted the synthetic `requires (param != 0)` as a
standalone spec block after calling WriteProcedureHeader, instead of
injecting it into the procedure's Requires list so WriteProcedureHeader
could emit everything in a single unified spec block.
Fix:
Build a synthetic Requires AST node (param != 0) and add it to
node.Requires before calling WriteProcedureHeader, then remove it
afterward to avoid permanently mutating the shared Boogie AST. This
lets WriteProcedureHeader's existing spec-emission loop handle both
the synthetic requires and any pre-existing specs in one block.
Test:
Added SmackAssertDuplicateSpec.bpl (assert_ procedure with an existing
ensures clause) and SmackAssertWithEnsuresProducesSingleSpecBlock test
that counts spec block occurrences and asserts exactly one is emitted.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two IdentifierExpr emission sites (EmitOldExpr and VisitExpr) used a ternary fallback: Decl != null ? NameOf(...) : Name(...). When Decl was null, this silently fell back to Name(), emitting the raw sanitized name without consulting the rename map -- wrong in name-collision scenarios. Post-Boogie-resolution, Decl is always non-null. Replace both fallbacks with explicit null checks that throw StrataConversionException, then always use NameOf(). Added OldExprRenameCollision.bpl and OldExprUsesRenamedNameOnCollision integration test verifying old() uses the renamed name on collision. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…difies clauses SMACK-generated Boogie can omit explicit modifies clauses. InferModifies=true causes Boogie's ModSetCollector to infer them, so the translator correctly emits globals as inout parameters. This test catches regressions if Boogie ever stops inferring or the option is accidentally disabled. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…Modifies Extract the ClaimOrRename local closure from EmitProgramAsStrata into a private static method on StrataGenerator, passing the claimed set and renames dictionary as explicit parameters. Documentation additions: - Expand _renames field comment to describe the registration order (procs first, then impls defensively, then consts/funcs/globals) - Add comment on implementation loop explaining it is defensive - Document InferModifies=true motivation in BoogieToStrata.cs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Thanks @tautschnig for your detailed comments. I have addressed everything except for the |
The VisitProcedure assert_.<type> handler adds a synthetic Requires to node.Requires before calling WriteProcedureHeader and removes it after. The unconditional removal at the end of the if-block leaks the synthetic Requires onto the shared Boogie AST if WriteProcedureHeader (or anything it transitively calls) throws. Wrap the WriteProcedureHeader call in try/finally so the synthetic Requires is always removed, matching the invariant the surrounding comment promises.
Without a .expect file, VerifyTestFile runs the .core.st through the verifier with --check (parse-only), which gives no end-to-end coverage that the renamed name in old() actually translates to a sound VC. Add a .expect with the expected single ensures-goal pass so the verification path runs and asserts an exact-match result, matching the standard the other regression tests in this PR follow.
Without a .expect file, VerifyTestFile runs with --check (parse-only). Add a .expect (`All 0 goals passed.` — the procedure has no specs or asserts) so the verifier actually consumes the .core.st output and the test exercises the full end-to-end path, matching the standard the other regression tests in this PR follow.
MikaelMayer
left a comment
There was a problem hiding this comment.
🤖🔍 Good progress since the last round. The .expect files have been added for all test cases, ClaimOrRename is now a static helper, and the assert_. pattern (with the dot) correctly avoids false positives like assert_helper. The rename mechanism is well-structured and the test coverage is solid.
One remaining issue below.
No behavior change. Plumbs a new bool smack param through EmitProgramAsStrata into the generator constructor, hardcoded to true at the single call site. Subsequent commits gate SMACK-specific behavior on this flag and wire it to a CLI option.
VisitProcedure now consults the _smack field set by the constructor. The Requires.Count == 0 guard is removed: when the assert_.<type> pattern matches under --smack, the synthetic `requires (p != 0)` is injected unconditionally, alongside any pre-existing requires/ensures. WriteProcedureHeader still emits a single merged spec block. Addresses MikaelMayer's review comment on PR #1149: the previous guard silently dropped the SMACK invariant when a pre-existing requires was present.
--smack enables two SMACK-specific accommodations: InferModifies=true (plus ModSetCollector mutation and CheckModifies suppression for missing modifies clauses) and the synthetic `requires (p != 0)` injection on assert_.<type> procedures. Without the flag, the translator runs in strict Boogie mode. Manual arg parsing (no new package dep). The flag may appear before or after the positional filename. Unknown flags or wrong arg count print the usage banner and exit 1.
RunTranslation(filePath) now reads the first 5 lines of the .bpl and
passes --smack when the literal token {:smack} is present. The new
RunTranslation(filePath, bool smack) overload is the explicit form,
used by tests that pin down the off-by-default behavior.
Adds the per-file {:smack} marker so the integration runner passes
--smack. Also removes the stale paragraph at lines 7-9 that described
pre-fix behavior ("currently produces 'All 0 goals passed'") — after
the assert_.<type> fix landed, the test now produces a verification
failure as captured in SmackAssert.expect.
Addresses MikaelMayer's review comment on PR #1149.
Adds a second assert_.<type> procedure (assert_.i32_with_req) with a
hand-written `requires (p.0 > -1)` and asserts that the output spec
block contains BOTH the synthetic `requires (p.0 != 0)` and the
user-written one. This pins down the post-fix behavior of dropping the
Requires.Count == 0 guard: the SMACK invariant is preserved
unconditionally, alongside any pre-existing requires.
Renames the test method to SmackAssertProducesSingleMergedSpecBlock to
reflect the broader coverage. Adds {:smack} marker to the .bpl.
Note: name sanitization rewrites `p.0` -> `p_0` and the unary literal
`-1` is emitted as `-(1)`, so the assertions match `p_0 != 0` and
`p_0 > -(1)` against the actual output.
Addresses MikaelMayer's review comment on PR #1149.
This regression test proves that even under --smack, names like assert_helper (no literal dot) do NOT receive the synthetic requires. The marker is required for the test to actually exercise the --smack-on path; without it, the test would only prove the gate is closed when --smack is off, which is uninteresting.
This file relies on InferModifies = true, which is now gated on the --smack flag. The marker tells the integration runner to pass --smack when translating this file. Without the flag, the file deliberately fails Boogie typechecking (no explicit `modifies g;` clause); a companion test in the next task pins down that strict-mode failure.
Two new facts assert that the SMACK-specific accommodations are gated
behind --smack:
1. SmackAssertWithoutFlagDoesNotInjectRequires — translates
SmackAssert.bpl without --smack and asserts the output has no
`requires` clause (the assert_.<type> pattern is treated as opaque).
2. InferModifiesOffWithoutSmackFlag — translates InferModifiesGlobal.bpl
without --smack and asserts the run exits non-zero (Boogie rejects
the program for the missing `modifies g;` clause).
MikaelMayer
left a comment
There was a problem hiding this comment.
🤖🔍 All previous comments appear addressed — reviewer sign-off still needed.
Issue #, if available: #1148
Description of changes:
Namespace collision (issue 1 in BoogieToStrata: Blockers preventing SMACK-generated Boogie verification #1148): Constants that share a name with a procedure are now prefixed with _const in the Strata output. The rename is applied consistently in declarations and references.
Recursive synonym resolution (Fixes issue 2 in BoogieToStrata: Blockers preventing SMACK-generated Boogie verification #1148): DealiasTypeExpr now recurses on its result, resolving synonym chains like ref := i64 := int fully to the base type. Previously it resolved only one level, causing panics on comparison/arithmetic operators applied to multi-level type synonyms.
SMACK assert encoding (issue 3 in BoogieToStrata: Blockers preventing SMACK-generated Boogie verification #1148): Procedures named assert_.* now get a requires (arg != 0) precondition emitted. Call elimination generates VCs at each call site, making SMACK assertions verifiable.
InferModifies: SMACK-generated Boogie often omits explicit
modifiesclauses on procedures that mutate globals. SettingInferModifies = truein BoogieToStrata has two effects: (1) it runsModSetCollector.CollectModifies(program)to populate empty modifies clauses, and (2) throughCheckModifiesin Boogie's resolution context, it suppresses typechecking of modifies clauses. Without this,ResolveAndTypecheckwould reject SMACK programs that omit modifies clauses.By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.