lean4-htt/tests/elab_fail/rewrite.lean
Joachim Breitner 06fb4bec52
feat: require indentation in commands, allow empty tactic sequences (#13229)
This PR wraps the top-level command parser with `withPosition` to
enforce indentation in `by` blocks, combined with an empty-by fallback
for better error messages.

This subsumes #3215 (which introduced `withPosition commandParser` but
without the empty-by fallback). It is also related to #9524, which
explores elaboration with empty tactic sequences — this PR reuses that
idea for the empty-by fallback, so that a `by` not followed by an
indented tactic produces an elaboration error (unsolved goals) rather
than a parse error.

**Changes:**
- `topLevelCommandParserFn` now uses `(withPosition commandParser).fn`,
setting the saved position at the start of each top-level command
- `tacticSeqIndentGt` gains an empty tactic sequence fallback
(`pushNone`) so that missing indentation produces an elaboration error
(unsolved goals) instead of a parse error
- `isEmptyBy` in `goalsAt?` removed: with strict `by` indentation, empty
`by` blocks parse successfully via `pushNone` (producing empty nodes)
rather than producing `.missing` syntax, making the `isEmptyBy` check
dead code. The `isEmpty` helper in `isSyntheticTacticCompletion`
continues to work correctly because it handles both `.missing` and empty
nodes from `pushNone` (via the vacuously-true `args.all isEmpty` on
`#[]`)
- Test files updated to indent `by` blocks and expression continuations
that were previously at column 0

**Behavior:**
- Top-level `by` blocks now require indentation (column > 0 for commands
at column 0)
- Commands indented inside `section` require proofs to be indented past
the command's column
- `#guard_msgs in example : True := by` works because tactic indentation
is checked against the outermost command's column
- Expression continuations (not just `by`) must also be indented past
the command, which is slightly more strict but more consistent
- `have : True := by` followed by a dedent now correctly puts `this` in
scope in the outer tactic block (the `have` is structurally complete
with an unsolved-goal error, rather than a parse error)

**Code changes observed in practice (lean4 test suite + Mathlib):**

- `by` blocks: top-level `theorem ... := by` / `decreasing_by` followed
by tactics at column 0 must be indented
- `variable` continuations: `variable {A : Type*} [Foo A]\n{B : Type*}`
where the second line starts at column 0 must be indented (most common
category in Mathlib)
- Expression continuations: `def f : T :=\nexpr` or `#synth Foo\n[args]`
where the body/arguments start at column 0
- Structure literals: `.symm\n{ toFun := ...` where the struct literal
starts at column 0

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 14:05:47 +00:00

113 lines
3.7 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/-!
# Tests exercising basic behaviour of `rw`.
See also `tests/lean/run/rewrite.lean`.
-/
axiom appendNil {α} (as : List α) : as ++ [] = as
axiom appendAssoc {α} (as bs cs : List α) : (as ++ bs) ++ cs = as ++ (bs ++ cs)
axiom reverseEq {α} (as : List α) : as.reverse.reverse = as
theorem ex1 {α} (as bs : List α) : as.reverse.reverse ++ [] ++ [] ++ bs ++ bs = as ++ (bs ++ bs) := by
rw [appendNil, appendNil, reverseEq];
trace_state;
rw [←appendAssoc];
theorem ex2 {α} (as bs : List α) : as.reverse.reverse ++ [] ++ [] ++ bs ++ bs = as ++ (bs ++ bs) := by
rewrite [reverseEq, reverseEq]; -- Error on second reverseEq
done
axiom zeroAdd (x : Nat) : 0 + x = x
theorem ex2a (x y z) (h₁ : 0 + x = y) (h₂ : 0 + y = z) : x = z := by
rewrite [zeroAdd] at h₁ h₂;
trace_state;
subst x;
subst y;
exact rfl
theorem ex3 (x y z) (h₁ : 0 + x = y) (h₂ : 0 + y = z) : x = z := by
rewrite [zeroAdd] at *;
subst x;
subst y;
exact rfl
theorem ex4 (x y z) (h₁ : 0 + x = y) (h₂ : 0 + y = z) : x = z := by
rewrite [appendAssoc] at *; -- Error
done
theorem ex5 (m n k : Nat) (h : 0 + n = m) (h : k = m) : k = n := by
rw [zeroAdd] at *;
trace_state; -- `h` is still a name for `h : k = m`
refine Eq.trans h ?hole;
apply Eq.symm;
assumption
theorem ex6 (p q r : Prop) (h₁ : q → r) (h₂ : p ↔ q) (h₃ : p) : r := by
rw [←h₂] at h₁;
exact h₁ h₃
theorem ex7 (p q r : Prop) (h₁ : q → r) (h₂ : p ↔ q) (h₃ : p) : r := by
rw [h₂] at h₃;
exact h₁ h₃
example (α : Type) (p : Prop) (a b c : α) (h : p → a = b) : a = c := by
rw [h _] -- should manifest goal `⊢ p`, like `rw [h]` would
/-!
Testing the `occs` configuration argument.
-/
variable (f : Nat → Nat) (w : ∀ n, f n = 0)
example : [f 1, f 2, f 1, f 2] = [0, 0, 0, 0] := by
rw (config := {occs := .pos [2]}) [w]
trace_state -- make sure we rewrote the first `f 2`, rather than the second `f 1`.
rw [w, w]
example : [f 1, f 2, f 1, f 2] = [0, 0, 0, 0] := by
rw (config := {occs := .all}) [w]
trace_state -- expecting [0, f 2, 0, f 2] = [0, 0, 0, 0]
rw [w]
example : [f 1, f 2, f 1, f 2] = [0, 0, 0, 0] := by
rw (config := {occs := .pos [1, 2]}) [w]
trace_state -- expecting [0, f 2, 0, f 2] = [0, 0, 0, 0]
-- After the first rewrite, the argument of `w` have been instantiated as `1`,
-- so the second eligible rewrite is the second `f 1`.
rw [w]
example : [f 1, f 2, f 1, f 2] = [0, 0, 0, 0] := by
rw (config := {occs := .neg [1]}) [w]
trace_state -- expecting [f 1, 0, f 1, 0] = [0, 0, 0, 0]
rw [w]
example : [f 1, f 2, f 1, f 2] = [0, 0, 0, 0] := by
rw (config := {occs := .neg [1, 2]}) [w]
trace_state -- expecting [f 1, f 2, 0, f 2] = [0, 0, 0, 0]
rw [w, w]
example : [f 1, f 2, f 1, f 2] = [0, 0, 0, 0] := by
rw (config := {occs := .neg [1, 3]}) [w]
trace_state -- expecting [f 1, 0, f 1, f 2] = [0, 0, 0, 0]
-- This one is slightly confusing:
-- We skipped the first `f 1`, because `1 ∈ [1,3]`.
-- We then rewrite the first `f 2 = 0`, because it is the second match.
-- Now the second `f 1` is no longer eligible, because we have instantiated the argument of `w` to `2`.
-- Finally we skip the second `f 2` because of `3 ∈ [1,3]`.
rw [w, w]
-- A slightly trickier example, where there are already metavariables in the goal,
-- and we ensure that we don't unify metavariables in positions skipped by `occs`.
example : { x : Nat × Nat // x.1 = 0 ∧ x.2 = 0 } := by
refine ⟨(f ?_, f ?_), ?_⟩
rotate_right
dsimp
rw (config := {occs := .pos [2]}) [w]
-- TODO: note that `rw` duplicates goals when there are metavariables.
-- This should be fixed.
rw [w]
exact ⟨rfl, rfl⟩
exact 37
exact 0