Skip to content

pyspec: add --reject-approximations CLI flag#1208

Open
julesmt wants to merge 7 commits into
main2from
julesmt/features/reject-approximations
Open

pyspec: add --reject-approximations CLI flag#1208
julesmt wants to merge 7 commits into
main2from
julesmt/features/reject-approximations

Conversation

@julesmt

@julesmt julesmt commented May 21, 2026

Copy link
Copy Markdown
Member

Adds --reject-approximations. Today some constructs silently lower to a Hole or .placeholder, so the verifier can report Analysis success on code whose contract was dropped. The flag turns those sites into hard errors. Off by default.

Strict mode for the Python frontend. Sites that silently lower
unsupported constructs to a Hole or drop a statement now raise
`unsupportedConstruct` (exit 4). Off by default.

Test: RejectApproximationsTest.lean.
@julesmt julesmt force-pushed the julesmt/features/reject-approximations branch from a852801 to e998c9d Compare May 27, 2026 00:22
@tautschnig

Copy link
Copy Markdown
Contributor

@keyboardDrummer-bot Please resolve git conflicts.

…ect-approximations

# Conflicts:
#	Strata/Languages/Python/PySpecPipeline.lean
#	Strata/Languages/Python/Specs.lean
#	Strata/Languages/Python/Specs/ToLaurel.lean
#	Strata/SimpleAPI.lean
#	StrataMain.lean

@tautschnig tautschnig left a comment

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.

Proof / #guard opportunities (none currently exist):

  • theorem rejectableHole_strict (c r) : (rejectableHole true c r).isOk = false and the dual …false…isOk = true — trivial decide-able lemmas (or #guards) that lock in the boolean dispatch.
  • theorem rejectableHole_strict_message (c r) : ∃ msg ast, rejectableHole true c r = .error (.unsupportedConstruct msg ast) ∧ msg.startsWith "[approximation] " — pins the message prefix so downstream tooling that filters by [approximation] can rely on it.

Minor stylistic nits:

  • "silently dropped today" — the today in rejectableDrop's message is informal and dates the wording. Either drop it or replace with "by current translation".
  • The two helper messages and Specs.lean's specWarning rewrite each have slightly different shapes (is approximated as Hole, is silently dropped today, [approximation] {message}). Once you have an end-to-end test from point 1, please consider unifying via a small approximationError : (kind : ApproxKind) → (construct : String) → String helper.

def mkInstanceMethodCall (className : String) (methodName : String)
For Any-typed receivers (no model available), returns a Hole under lax
mode, or raises `unsupportedConstruct` under `--reject-approximations`. -/
def mkInstanceMethodCall (rejectApproximations : Bool) (className : String) (methodName : String)

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.

After this PR, mkInstanceMethodCall is called from exactly two sites (the with-statement __enter__/__exit__ pair at lines 2059–2060), both of which pass ctx.rejectApproximations. Threading the bool explicitly is noise — please take the whole ctx : TranslationContext so the signature shrinks and future additions can read other fields too.

Also: the inline |>.map (fun h => { h with source := source }) in the Any branch is a workaround because rejectableHole ignores source. Cleaner to add a source : Option FileRange := none argument to rejectableHole itself and let it stamp the location in one place — the rejection error then carries the same source you already have, which makes the diagnostic much more useful under --reject-approximations.

Comment on lines +217 to +238
otherwise approximate an unsupported Python construct as a havoc'd
Hole. The `construct` argument names the surface form (e.g.,
`"BinOp Div"`) for the error message. -/
def rejectableHole (rejectApproximations : Bool) (construct astRepr : String)
: Except TranslationError StmtExprMd :=
if rejectApproximations then
throw (.unsupportedConstruct
s!"[approximation] {construct} is approximated as Hole; not in the sound subset"
astRepr)
else
pure (mkStmtExprMd .Hole)

/-- Same as `rejectableHole` but for sites that silently drop a Python
statement (e.g., `raise`, `try…else`, `for…else`) under lax mode.
The error message names the dropped construct. -/
def rejectableDrop (rejectApproximations : Bool) (construct astRepr : String)
: Except TranslationError Unit :=
if rejectApproximations then
throw (.unsupportedConstruct
s!"[approximation] {construct} is silently dropped today; not in the sound subset"
astRepr)
else

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.

Two callers pass (toString (repr e)) for astRepr. For deeply nested expressions this produces multi-kilobyte error strings — fine for unit tests, painful for users running on real specs. Consider taking (source : Option FileRange) instead and letting the diagnostic infrastructure render file:line:col plus a short construct name; the surface form is rarely useful when the location is known.

If you want to keep the AST repr as a fallback, capping it (astRepr.take 200 ++ "…") would prevent the worst cases without losing the unit-test guarantees.

Comment on lines 303 to +313
specError loc message := do
specErrorAt (←read).pythonFile loc message
specWarning loc message := do
let file := (←read).pythonFile
let w : PipelineMessage := { file, loc, phase := pySpecParsingPhase, kind := .pySpecParsingWarning, message }
modify fun s => { s with warnings := s.warnings.push w }
let ctx ← read
-- Under strict mode (`--reject-approximations`), every warning is an
-- error: the parser only emits warnings on patterns it falls back to
-- `.placeholder` for, which silently drops the user's spec.
if ctx.rejectApproximations then
specErrorAt ctx.pythonFile loc s!"[approximation] {message}"
else
let w : PipelineMessage := { file := ctx.pythonFile, loc, phase := pySpecParsingPhase, kind := .pySpecParsingWarning, message }

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.

Same logical change as rejectableHole/rejectableDrop in PythonToLaurel.lean, but expressed differently (warning→error rewrite vs. helper-with-throw). With rejectApproximations now in both TranslationContext and PySpecContext, please factor the message format into a single helper module (e.g. Strata/Languages/Python/Approximation.lean) so the prefix [approximation] is only typed once and the three current call shapes can't drift.

No regression test currently fires this specError arm — please add one alongside the existing Specs/... tests so the warning-to-error promotion is exercised.

keyboardDrummer-bot and others added 5 commits May 29, 2026 13:33
The strict-mode error helpers used to dump the full Python AST `repr`
into every diagnostic, producing multi-kilobyte messages on real specs.
Take an `Option FileRange` instead and render `file:offset` when one is
available; cap the AST string fallback at 200 chars. Also stamp the
returned `Hole` with the source so callers don't have to.

Reword `"silently dropped today"` → `"silently dropped by current
translation"` so the message doesn't read as date-dependent.
`mkInstanceMethodCall` took a single `rejectApproximations : Bool`
parameter; both call sites just forwarded `ctx.rejectApproximations`.
Take the whole context so future fields don't each grow a new bool
parameter.
Three places currently spell out the `[approximation] ` prefix and the
strict-mode wording inline: `rejectableHole`, `rejectableDrop`, and the
`specWarning` warning→error promotion in `Specs.lean`. They were
already drifting.

Add a small `Strata.Languages.Python.Approximation` module with a
`Kind` (`hole | drop | warningPromotion`), a single `prefixTag`, and a
`render` function. All three sites now route through it. New sites
won't need to retype the prefix.
Add four `rfl`-provable lemmas that pin the strict/lax branch of
`rejectableHole` and `rejectableDrop`, plus existence lemmas pinning
the strict-mode message to `Approximation.render`. A future refactor
that flips a branch or drifts the prefix won't compile.
@keyboardDrummer-bot

Copy link
Copy Markdown
Collaborator

@tautschnig All of the items you've listed appear to already be addressed in the current state of the PR:

  1. Proof / #guard opportunitiesStrataTest/Languages/Python/RejectApproximationsTest.lean already contains:

    • theorem rejectableHole_strict / rejectableHole_lax and the Drop equivalents (proved by rfl).
    • theorem rejectableHole_strict_message / rejectableDrop_strict_message proving the error is .unsupportedConstruct with msg = Approximation.render .hole c.
    • Multiple #guards that verify prefixTag, per-kind wording, and that strict-mode messages match render.
  2. Stylistic nit ("today")Approximation.render .drop already uses "silently dropped by current translation", not "today".

  3. Unified helperStrata/Languages/Python/Approximation.lean defines prefixTag and render : Kind → String → String as a single source of truth. All three call shapes (rejectableHole, rejectableDrop, and Specs.lean's specWarning promotion) route through it.

Is there anything else you'd like changed here, or can these review threads be resolved?

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.

3 participants