Skip to content

Support transparent procedures#1215

Open
keyboardDrummer wants to merge 17 commits into
strata-org:formatting-and-debugging-improvementsfrom
keyboardDrummer:transparency-pass-only
Open

Support transparent procedures#1215
keyboardDrummer wants to merge 17 commits into
strata-org:formatting-and-debugging-improvementsfrom
keyboardDrummer:transparency-pass-only

Conversation

@keyboardDrummer
Copy link
Copy Markdown
Contributor

@keyboardDrummer keyboardDrummer commented May 26, 2026

DO NOT MERGE - because the base is not main2

Depends on

Base PR for:

Changes

  • Replace a panic in SMTUtils with a .error
  • Change handling of free postconditions in Core so they do not generate trivial VCs. This is useful in this PR because this PR is adding new usages of free postconditions and this way existing tests don't get additional "passing" VCs. A side-effect of this change is that existing tests have fewer reported passing VCs.
  • Improve usage of source locations in the lifting phase, which improves diagnostics
  • For procedure without a source body, use free postconditions instead of assuming the non-free postconditions in a generated body.
  • Add the transparency pass. for each non-functional Core procedure, generate a function with the same signature and name suffixed with $asFunction. If a Core procedure is marked as transparent, attempt to generate a functional body for the $asFunction, where assertions are erased and all calls are to functional versions. Tie the functional version to the procedure using a free postcondition.
  • Refactored CoreGroupingAndOrdering to compute SCC ordering for functions and procedures separately. This is required because TransparencyPass creates a new AST type, UnorderedCoreWithLaurelTypes, where functions and procedures are already separated.

Testing

  • Updated T20_TransparentBody to account for new feature. In a follow-up we will have many more tests, because we'll convert all functions in tests to procedures.
  • Added T20_TransparentBodyError to capture some not supported cases

@keyboardDrummer keyboardDrummer changed the title Transparency pass only Support transparent procedures May 26, 2026
@keyboardDrummer keyboardDrummer changed the base branch from formatting-and-debugging-improvements to main May 26, 2026 12:38
@keyboardDrummer keyboardDrummer changed the base branch from main to main2 May 26, 2026 12:39
@keyboardDrummer keyboardDrummer changed the base branch from main2 to formatting-and-debugging-improvements May 27, 2026 10:19
@keyboardDrummer keyboardDrummer marked this pull request as ready for review May 28, 2026 09:34
@keyboardDrummer keyboardDrummer requested a review from a team as a code owner May 28, 2026 09:34
Copy link
Copy Markdown
Contributor

@joscoh joscoh left a comment

Choose a reason for hiding this comment

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

What happens with transparent recursive procedures? Does it produce a recursive function? If so, what arguments are given for the termination checker, and what happens if the generated function is nonterminating?

let mut allDiagnostics := passDiags ++ translateState.diagnostics
runTranslateM initState (translateLaurelToCore options program coreWithLaurelTypes)
-- Because of the duplication between functions and procedures, this translation is liable to create duplicate diagnostics
-- User errors should be checked in an earlier phase, and all dumb translation errors are Strata bugs
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What does this mean?

let tyVarName := s!"$__ty_unused_{id}"
let coreType := LTy.forAll [tyVarName] (.ftvar tyVarName)
let ty ← translateType (computeExprType model expr)
let coreType := LTy.forAll [] ty
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

How do you know the type is never polymorphic?

- `proof = false` → use unsafe selectors (add `!` suffix) -/
private def adjustSelectorName (name : String) : String :=
-- Only adjust destructor names (contain ".." but are not testers)
match name.splitOn ".." with
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Minor, but I would prefer having isDestructorName and isTesterName functions in TypeFactory.lean that do this check, to ensure that there aren't silent changes in Core naming that break this.


/-- Build a free postcondition equating the procedure's output to its functional version.
For a procedure `foo(a, b) returns (r)`, produces:
`r == foo$asFunction(a, b)` -/
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

IIUC, this pass constructs functions intended to be equivalent to the corresponding procedure and then assumes the equivalence. Is it possible to prove the equivalence instead?

Copy link
Copy Markdown
Contributor

@joscoh joscoh left a comment

Choose a reason for hiding this comment

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

It would also be good to see a test where a transparent procedure calls another transparent procedure, with the corresponding function calls

{ proc with name := funcName, isFunctional := true, body := body, preconditions := [] }

/-- Check whether a function copy has a body (i.e. the procedure was transparent). -/
private def functionHasBody (proc : Procedure) : Bool :=
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this function ever called?

: UnorderedCoreWithLaurelTypes × SemanticModel :=
let fnProgram := unorderedCoreToProgram uc additionalTypes
let fnResolveResult := resolve fnProgram existingModel
(fromResolvedProgram fnResolveResult.program, fnResolveResult.model)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This silently drops any errors encountered from resolution. Is this intentional?

⟨.StaticCall funcCallee args, e.source⟩
else
let newName := adjustSelectorName callee.text
⟨ .StaticCall newName args, e.source⟩
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This drops any metadata associated with the callee (e.g. source location).

mapStmtExpr (fun e =>
match e.val with
| .StaticCall callee args =>
if asFunctionNames.contains callee.text then
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A HashSet would be more efficient, but for small programs this is OK

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.

🤖🔍 Overall this is a well-structured PR that introduces a clean intermediate representation (UnorderedCoreWithLaurelTypes) and a transparency pass that generates functional versions of procedures. The pipeline extension is logical and the test coverage is reasonable.

Positive notes:

  • The stripAssertAssume / rewriteCallsToFunctional / mkFreePostcondition decomposition is clean and easy to follow.
  • The change from .assert to .assume for free postconditions in ProcedureEval.lean is correct — free postconditions should not generate proof obligations.
  • Replacing panic! with .error in SMTUtils.lean is a good improvement.

See inline comments for issues found.

match proc.body with
| .Opaque postconds _ _ | .Abstract postconds =>
translateChecks postconds "postcondition"
translateChecks postconds s!"postcondition{(bodyStmts.getD []).length}" bodyStmts.isNone
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖🔍 The label s!"postcondition{(bodyStmts.getD []).length}" encodes the body statement count into the label base (e.g. "postcondition5"). This produces labels like postcondition5_0, postcondition5_1 for multiple postconditions. This seems like a workaround to avoid label collisions — could you add a comment explaining why the body length is embedded in the label name?

@@ -770,6 +770,7 @@ def translateLaurelToCore (options: LaurelTranslateOptions) (program : Program)
emitDiagnostic $ diagnosticFromSource proc.name.source
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖🔍 This "Instance procedure ... is not yet supported" diagnostic is now emitted in two places: here and in Resolution.lean (the new loop at the top of resolve). This duplication is likely why .eraseDups was added to the pipeline. Consider removing one of the two emission sites rather than relying on deduplication downstream.

runTranslateM initState (translateLaurelToCore options program coreWithLaurelTypes)
-- Because of the duplication between functions and procedures, this translation is liable to create duplicate diagnostics
-- User errors should be checked in an earlier phase, and all dumb translation errors are Strata bugs
let mut allDiagnostics: List DiagnosticModel := passDiags ++ translateState.diagnostics.eraseDups;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖🔍 Using .eraseDups to suppress duplicate diagnostics is a symptom, not a fix. The root cause is that the same diagnostic (e.g. "Instance procedure ... not yet supported") is emitted in both Resolution.lean and LaurelToCoreTranslator.lean. Fixing the duplication at the source would be cleaner than filtering after the fact.

-- Skip functions until we introduce a contract pass,
-- which enables lifting procedure calls from contracts
p.isFunctional)
let asFunctionNames := notSkipped.map (fun p => p.name.text)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖🔍 mkFreePostcondition returns LiteralBool true when proc.outputs is not a singleton. This means procedures with zero or multiple outputs silently get no free postcondition tying them to their functional version. The zero-output case is fine (no return value to equate), but the multi-output case could be supported with a conjunction of equalities. Is this intentional or a TODO?

for diag in unmatchedDiagnostics do
IO.println s!" - Line {diag.start.line}, Col {diag.start.column}-{diag.ending.column}: {diag.message}"

if unmatchedExpectations.length == 0 && unmatchedDiagnostics.length == 0 then
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖🔍 This branch fires when diagnostics.size != expectedErrors.length but both unmatchedExpectations and unmatchedDiagnostics are empty — which means there are duplicate diagnostics matching the same expectations. The message "Duplicate diagnostics: {repr diagnostics}" dumps the full repr which could be very noisy. Consider printing just the count or the duplicate entries.


/--
Transparency pass: translate a Laurel program to the UnorderedCoreWithLaurelTypes IR.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖🔍 transparencyPass always adds a free postcondition to transparent procedures (via addFreePostcondition), which promotes their .Transparent body to .Opaque. This means the original transparent body is now wrapped in an Opaque with an implementation — so the procedure is no longer "transparent" in the Laurel sense after this pass. This is fine architecturally (the function copy carries the transparent semantics), but the docstring on addFreePostcondition could note that this promotion is the expected behavior for transparent procedures.

@atomb
Copy link
Copy Markdown
Contributor

atomb commented May 28, 2026

To optimize for reusability, I'd be happier to have a procedure -> function transformation exist at the Core level rather than the Laurel level. It will likely be useful for front-end languages that don't go through Laurel, too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants