Skip to content

Fix namespace collision and SMACK assert encoding in BoogieToStrata#1149

Open
PROgram52bc wants to merge 21 commits into
mainfrom
htd/smack-fix
Open

Fix namespace collision and SMACK assert encoding in BoogieToStrata#1149
PROgram52bc wants to merge 21 commits into
mainfrom
htd/smack-fix

Conversation

@PROgram52bc
Copy link
Copy Markdown
Contributor

@PROgram52bc PROgram52bc commented May 8, 2026

Issue #, if available: #1148

Description of changes:

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

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

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

  4. InferModifies: SMACK-generated Boogie often omits explicit modifies clauses on procedures that mutate globals. Setting InferModifies = true in BoogieToStrata has two effects: (1) it runs ModSetCollector.CollectModifies(program) to populate empty modifies clauses, and (2) through CheckModifies in Boogie's resolution context, it suppresses typechecking of modifies clauses. Without this, ResolveAndTypecheck would 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.

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>
@PROgram52bc PROgram52bc enabled auto-merge May 8, 2026 21:24
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>
Copy link
Copy Markdown
Contributor

@tautschnig tautschnig left a comment

Choose a reason for hiding this comment

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

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. VerifyTestFile adds --check when no .expect file exists, which literally emits Skipping verification. without invoking the solver. I re-ran the three new tests: with no .expect, SmackAssert.bpl passes CI even though the whole point of the test is that call $r := assert_.i32(0); should now fail verification. Please add .expect files for all three new tests (SmackAssert.expect crucially must contain a failed line so the harness flips expectedExitCode to 2 and actually catches regressions).

  • Fix (2) has no regression test. dealiasTypeExpr now recurses, but there is no .bpl test exercising a multi-level synonym chain (the issue's reproducer TypeSynonymComparison.core.st wasn't carried over). Existing TypeSynonyms{1,2}.bpl only use single-level synonyms. Without a test, this fix has no CI coverage. A 10-line .bpl using type i64 = int; type ref = i64; with <= / + over ref would close this.

  • Rename mechanism is not applied consistently. Four of the places that emit variable names still call Name(...) instead of NameOf(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 verify reports Unknown expr identifier __var__a_b. See the inline comment at VisitCallCmd for 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); on assert_helper even though the user never wrote it. Any Boogie program with a procedure whose name happens to start with assert_ (e.g. assert_helper, assert_invariant, assert_equal) would start failing verification. SMACK's actual pattern is assert_.<type> (literal dot); please tighten to match that specifically.

Non-blocking

  • InferModifies = true isn't mentioned in the PR body. It's not just a knob — it runs ModSetCollector (mutating the program) and, via CheckModifies, suppresses modifies-clause typechecking. Please document the motivation and add a .bpl test with a procedure that mutates a global without an explicit modifies clause, so regressions in this new assumption are caught.
  • Proof opportunities for dealiasTypeExpr. Since this is partial def, strong termination isn't provable without an acyclicity argument on p.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: if dealiasTypeExpr p te is .fvar _ idx #[], then p.globalContext.kindOf! idx is neither .expr _ nor .type [] (.some _). If you don't want to prove these, at least consider a Lean-side #guard test constructing a three-level synonym Program and checking the result is .int.
  • Refactoring the rename builder. The ClaimOrRename logic would be more readable as a named static helper that takes the claimed set and the _renames dict as parameters, rather than a closure inside EmitProgramAsStrata. The ordering "procs → impls → consts → funcs → globals" is a deliberate tie-breaker (first-seen wins) — please document it at the _renames field, 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 new spec { requires ... } block in VisitProcedure duplicates the emission pattern in WriteProcedureHeader. Consider injecting a synthetic Requires into node.Requires just before calling WriteProcedureHeader so the existing code path handles it uniformly.
  • IdentifierExpr fallback. identExpr.Decl != null ? NameOf(...) : Name(...): post-Boogie-resolution, Decl should always be non-null. Silently falling back to Name(...) emits the unrenamed name and is wrong in the collision case. Consider asserting non-null instead, or at least add a comment about when Decl can be null and why the fallback is safe.

Comment thread Strata/Languages/Core/DDMTransform/Translate.lean
Comment thread Strata/Languages/Core/DDMTransform/Translate.lean
Comment thread Tools/BoogieToStrata/Source/BoogieToStrata.cs Outdated
Comment thread Tools/BoogieToStrata/Source/StrataGenerator.cs
Comment thread Tools/BoogieToStrata/Source/StrataGenerator.cs Outdated
Comment thread Tools/BoogieToStrata/Source/StrataGenerator.cs Outdated
Comment thread Tools/BoogieToStrata/Source/StrataGenerator.cs Outdated
Comment thread Tools/BoogieToStrata/Tests/SmackAssert.bpl
Comment thread Tools/BoogieToStrata/Tests/NamespaceCollision.bpl
Comment thread Tools/BoogieToStrata/Tests/SanitizationCollision.bpl
David Deng and others added 5 commits May 11, 2026 10:11
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>
@PROgram52bc
Copy link
Copy Markdown
Contributor Author

Thanks @tautschnig for your detailed comments. I have addressed everything except for the dealiasTypeExpr proof, which could be completed in a separate PR, especially in light of the refactoring that's going to take place.

David Deng added 3 commits May 18, 2026 10:44
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.
atomb
atomb previously approved these changes May 18, 2026
Comment thread Tools/BoogieToStrata/Source/StrataGenerator.cs Outdated
Copy link
Copy Markdown
Contributor

@MikaelMayer MikaelMayer left a comment

Choose a reason for hiding this comment

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

🤖🔍 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.

Comment thread Tools/BoogieToStrata/Source/StrataGenerator.cs Outdated
Comment thread Tools/BoogieToStrata/Tests/SmackAssert.bpl Outdated
David Deng added 2 commits May 19, 2026 14:58
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.
David Deng added 7 commits May 19, 2026 15:14
--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).
Copy link
Copy Markdown
Contributor

@MikaelMayer MikaelMayer left a comment

Choose a reason for hiding this comment

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

🤖🔍 All previous comments appear addressed — reviewer sign-off still needed.

@PROgram52bc PROgram52bc requested review from MikaelMayer and atomb May 19, 2026 23:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

4 participants