Support transparent procedures#1215
Conversation
…trata into transparency-pass-only
joscoh
left a comment
There was a problem hiding this comment.
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 |
| let tyVarName := s!"$__ty_unused_{id}" | ||
| let coreType := LTy.forAll [tyVarName] (.ftvar tyVarName) | ||
| let ty ← translateType (computeExprType model expr) | ||
| let coreType := LTy.forAll [] ty |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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)` -/ |
There was a problem hiding this comment.
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?
joscoh
left a comment
There was a problem hiding this comment.
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 := |
There was a problem hiding this comment.
Is this function ever called?
| : UnorderedCoreWithLaurelTypes × SemanticModel := | ||
| let fnProgram := unorderedCoreToProgram uc additionalTypes | ||
| let fnResolveResult := resolve fnProgram existingModel | ||
| (fromResolvedProgram fnResolveResult.program, fnResolveResult.model) |
There was a problem hiding this comment.
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⟩ |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
A HashSet would be more efficient, but for small programs this is OK
MikaelMayer
left a comment
There was a problem hiding this comment.
🤖🔍 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/mkFreePostconditiondecomposition is clean and easy to follow. - The change from
.assertto.assumefor free postconditions inProcedureEval.leanis correct — free postconditions should not generate proof obligations. - Replacing
panic!with.errorinSMTUtils.leanis 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 |
There was a problem hiding this comment.
🤖🔍 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 | |||
There was a problem hiding this comment.
🤖🔍 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; |
There was a problem hiding this comment.
🤖🔍 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) |
There was a problem hiding this comment.
🤖🔍 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 |
There was a problem hiding this comment.
🤖🔍 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. | ||
|
|
There was a problem hiding this comment.
🤖🔍 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.
|
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. |
DO NOT MERGE - because the base is not main2
Depends on
Base PR for:
Changes
.error$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.UnorderedCoreWithLaurelTypes, where functions and procedures are already separated.Testing