@@ -1097,14 +1097,72 @@ fn subst_occurrences(goal_nf: &Expr, from_nf: &Expr, to: &Expr) -> Expr {
10971097 sys. nf ( goal_nf)
10981098}
10991099
1100- /// Report a `rewrite(<eq>)?` suggestion hole. Given the equation `lhs = rhs` the
1101- /// argument proves and the current goal, synthesize the `forward` motive automatically
1102- /// — abstracting *every* occurrence of `lhs` in the goal — and, when the resulting
1103- /// `forward` step actually applies, offer it as a paste-able fix
1104- /// `by forward(T, lhs, rhs, <eq>, P) then <ctx> ⊢ <newgoal>;`. When the rewrite does
1105- /// not apply (no matching subterm, un-inferrable type, non-equation argument, or
1106- /// `forward` not in scope), emit a plain diagnostic with no fix. The step is admitted,
1107- /// so the proof stays incomplete — exactly like every other `?` inspect step.
1100+ /// Immediate subexpressions of `e`, for subterm traversal.
1101+ fn expr_children ( e : & Expr ) -> Vec < & Expr > {
1102+ match e {
1103+ Expr :: App ( f, args) => {
1104+ let mut v = Vec :: with_capacity ( args. len ( ) + 1 ) ;
1105+ v. push ( f. as_ref ( ) ) ;
1106+ v. extend ( args. iter ( ) ) ;
1107+ v
1108+ }
1109+ Expr :: Lam ( a, b)
1110+ | Expr :: Forall ( a, b)
1111+ | Expr :: Pi ( a, b)
1112+ | Expr :: Exists ( a, b)
1113+ | Expr :: Arrow ( a, b)
1114+ | Expr :: Eq ( a, b)
1115+ | Expr :: And ( a, b)
1116+ | Expr :: Or ( a, b)
1117+ | Expr :: Implies ( a, b)
1118+ | Expr :: Iff ( a, b) => vec ! [ a. as_ref( ) , b. as_ref( ) ] ,
1119+ Expr :: Not ( a) => vec ! [ a. as_ref( ) ] ,
1120+ Expr :: Product ( xs) | Expr :: Sum ( xs) => xs. iter ( ) . collect ( ) ,
1121+ Expr :: Bound ( _) | Expr :: Free ( _) | Expr :: Const ( _) | Expr :: Sort | Expr :: Prop | Expr :: False => {
1122+ Vec :: new ( )
1123+ }
1124+ }
1125+ }
1126+
1127+ /// The metavariable binding from the first (pre-order, leftmost-outermost) subterm
1128+ /// of `goal` that matches `pat` (with `metas` as wildcards), or `None`.
1129+ fn first_subterm_match ( goal : & Expr , pat : & Expr , metas : & [ Sym ] ) -> Option < Vec < ( Sym , Expr ) > > {
1130+ let mut subst = Vec :: new ( ) ;
1131+ if crate :: core:: rewrite:: match_pattern ( pat, goal, metas, & mut subst) {
1132+ return Some ( subst) ;
1133+ }
1134+ for child in expr_children ( goal) {
1135+ if let Some ( s) = first_subterm_match ( child, pat, metas) {
1136+ return Some ( s) ;
1137+ }
1138+ }
1139+ None
1140+ }
1141+
1142+ /// Format a `by forward(...)` call across lines, one argument per line indented two
1143+ /// spaces past the `by`. The first line carries no leading indent (it is spliced in
1144+ /// at the `by`'s existing column); callers prefix `indent` when embedding it in a
1145+ /// message.
1146+ fn format_forward_call ( indent : & str , arg_strs : & [ String ] ) -> String {
1147+ let arg_indent = format ! ( "{indent} " ) ;
1148+ let body = arg_strs. join ( & format ! ( ",\n {arg_indent}" ) ) ;
1149+ format ! ( "by forward(\n {arg_indent}{body})" )
1150+ }
1151+
1152+ /// Report a `rewrite(<eq>)?` suggestion hole. The argument names an equation
1153+ /// `lhs = rhs` — either fully applied (`pop_push(A, a, s)`) or with some/all
1154+ /// arguments left as holes (`pop_push` bare, or `pop_push(?A, ?x, ?s)`).
1155+ ///
1156+ /// * With holes, the missing arguments are **inferred** by matching the axiom's
1157+ /// left side against a subterm of the goal, and the suggestion is the same
1158+ /// `rewrite` step with those arguments filled in (`rewrite(pop_push(A, a, s))?`).
1159+ /// * Fully applied, the motive is synthesized automatically — abstracting *every*
1160+ /// occurrence of `lhs` in the goal — and the suggestion is the paste-able
1161+ /// `by forward(T, lhs, rhs, <eq>, P) then <ctx> ⊢ <newgoal>;` step.
1162+ ///
1163+ /// When nothing applies (no matching subterm, an argument that can't be inferred,
1164+ /// a non-equation argument, or `forward` not in scope) a plain diagnostic explains
1165+ /// why and offers no fix. The step is admitted, so the proof stays incomplete.
11081166fn report_rewrite_hole (
11091167 elab : & mut Elab ,
11101168 ctx : & [ CtxEntry ] ,
@@ -1113,15 +1171,15 @@ fn report_rewrite_hole(
11131171 rs : & RewriteSystem ,
11141172) {
11151173 use crate :: core:: display:: show;
1116- use crate :: core:: tactic:: apply;
1174+ use crate :: core:: tactic:: { apply, subst_all } ;
11171175
11181176 let reference = stmt. reference . as_ref ( ) . expect ( "inspect step has a reference" ) ;
11191177 let mut msg = format ! (
11201178 "found rewrite hole in `by rewrite`\n \n Goal:\n {}\n " ,
11211179 show( goal, & elab. interner)
11221180 ) ;
11231181
1124- // Exactly one argument: the equation proof.
1182+ // Exactly one argument: the equation (a proof reference, possibly with holes) .
11251183 if reference. args . len ( ) != 1 {
11261184 elab. err (
11271185 format ! (
@@ -1132,23 +1190,171 @@ fn report_rewrite_hole(
11321190 ) ;
11331191 return ;
11341192 }
1193+ let axiom_arg = & reference. args [ 0 ] ;
11351194
1136- // Resolve the argument to the equation it proves.
1137- let mut scope = scope_from_ctx ( ctx) ;
1138- let eq_stmt = match resolve_proof_term ( elab, ctx, & mut scope, & reference. args [ 0 ] ) {
1139- Some ( e) => e,
1140- None => return , // resolve_proof_term already reported the failure.
1195+ // Split the argument into its head reference and its surface arguments.
1196+ let ( qname, surface_args) = match & axiom_arg. node {
1197+ ast:: ExprNode :: Var ( q) => ( q. clone ( ) , Vec :: new ( ) ) ,
1198+ ast:: ExprNode :: App ( head, a) => match & head. node {
1199+ ast:: ExprNode :: Var ( q) => ( q. clone ( ) , a. clone ( ) ) ,
1200+ _ => {
1201+ elab. err ( "expected a proof reference" , axiom_arg. span ) ;
1202+ return ;
1203+ }
1204+ } ,
1205+ _ => {
1206+ elab. err ( "expected a proof reference" , axiom_arg. span ) ;
1207+ return ;
1208+ }
1209+ } ;
1210+
1211+ // Resolve the equation's rule (a global axiom/lemma or a local hypothesis).
1212+ let pref = ast:: ProofRef {
1213+ name : qname. clone ( ) ,
1214+ args : Vec :: new ( ) ,
1215+ span : axiom_arg. span ,
1216+ } ;
1217+ let ( _akey, arule) = match resolve_tactic ( elab, ctx, & pref) {
1218+ Some ( x) => x,
1219+ None => return ,
11411220 } ;
1142- let ( lhs, rhs) = match rs. nf ( & eq_stmt) {
1143- Expr :: Eq ( a, b) => ( * a, * b) ,
1221+ if !arule. premises . is_empty ( ) {
1222+ elab. err ( "a `rewrite` argument must reference a fact (no premises)" , axiom_arg. span ) ;
1223+ return ;
1224+ }
1225+ match & arule. conclusion {
1226+ Expr :: Eq ( _, _) => { }
11441227 other => {
11451228 msg. push_str ( & format ! (
11461229 "\n The argument proves `{}`, which is not an equation `a = b`.\n " ,
1147- show( & other, & elab. interner)
1230+ show( other, & elab. interner)
11481231 ) ) ;
11491232 elab. err ( msg. trim_end ( ) . to_string ( ) , stmt. span ) ;
11501233 return ;
11511234 }
1235+ }
1236+
1237+ let mut scope = scope_from_ctx ( ctx) ;
1238+ // Classify each parameter as a supplied value (→ `concrete`) or a hole to infer
1239+ // (→ `metas`). A bare reference leaves every parameter a hole.
1240+ let mut concrete_subst: Vec < ( Sym , Expr ) > = Vec :: new ( ) ;
1241+ let mut metas: Vec < Sym > = Vec :: new ( ) ;
1242+ let n = arule. params . len ( ) ;
1243+ if surface_args. is_empty ( ) {
1244+ for p in & arule. params {
1245+ match p {
1246+ Param :: Term { name, .. } => metas. push ( * name) ,
1247+ Param :: Proof { .. } => {
1248+ elab. err ( "`rewrite` cannot infer proof arguments — supply them explicitly" , axiom_arg. span ) ;
1249+ return ;
1250+ }
1251+ }
1252+ }
1253+ } else {
1254+ if surface_args. len ( ) != n {
1255+ elab. err (
1256+ format ! ( "`{}` takes {} argument(s), got {}" , qname. name. text, n, surface_args. len( ) ) ,
1257+ axiom_arg. span ,
1258+ ) ;
1259+ return ;
1260+ }
1261+ for ( p, sa) in arule. params . iter ( ) . zip ( & surface_args) {
1262+ let is_hole = matches ! ( sa. node, ast:: ExprNode :: NamedHole ( _) ) ;
1263+ match p {
1264+ Param :: Term { name, .. } => {
1265+ if is_hole {
1266+ metas. push ( * name) ;
1267+ } else {
1268+ match elab. lower_expr ( & mut scope, sa) {
1269+ Ok ( v) => concrete_subst. push ( ( * name, v) ) ,
1270+ Err ( _) => return ,
1271+ }
1272+ }
1273+ }
1274+ Param :: Proof { .. } => {
1275+ if is_hole {
1276+ elab. err ( "`rewrite` cannot infer proof arguments — supply them explicitly" , sa. span ) ;
1277+ return ;
1278+ }
1279+ let _ = resolve_proof_term ( elab, ctx, & mut scope, sa) ;
1280+ }
1281+ }
1282+ }
1283+ }
1284+
1285+ let goal_nf = rs. nf ( goal) ;
1286+ let ( lhs_pat, rhs_pat) = match & arule. conclusion {
1287+ Expr :: Eq ( a, b) => ( ( * * a) . clone ( ) , ( * * b) . clone ( ) ) ,
1288+ _ => unreachable ! ( "conclusion checked to be an equation above" ) ,
1289+ } ;
1290+
1291+ // --- Inference mode: solve the holes by matching the axiom's left side. ---
1292+ if !metas. is_empty ( ) {
1293+ if arule. params . iter ( ) . any ( |p| matches ! ( p, Param :: Proof { .. } ) ) {
1294+ elab. err ( "`rewrite` can only infer arguments for equations without proof hypotheses" , axiom_arg. span ) ;
1295+ return ;
1296+ }
1297+ let lhs_inst = rs. nf ( & subst_all ( & lhs_pat, & concrete_subst) ) ;
1298+ let solved = match first_subterm_match ( & goal_nf, & lhs_inst, & metas) {
1299+ Some ( s) => s,
1300+ None => {
1301+ msg. push_str ( & format ! (
1302+ "\n No subterm of the goal matches the left side `{}` of the equation.\n " ,
1303+ show( & lhs_inst, & elab. interner)
1304+ ) ) ;
1305+ elab. err ( msg. trim_end ( ) . to_string ( ) , stmt. span ) ;
1306+ return ;
1307+ }
1308+ } ;
1309+ let full: Vec < ( Sym , Expr ) > = concrete_subst. iter ( ) . chain ( solved. iter ( ) ) . cloned ( ) . collect ( ) ;
1310+ let unsolved: Vec < Sym > = metas. iter ( ) . copied ( ) . filter ( |m| !full. iter ( ) . any ( |( s, _) | s == m) ) . collect ( ) ;
1311+ if !unsolved. is_empty ( ) {
1312+ let names = unsolved
1313+ . iter ( )
1314+ . map ( |s| elab. interner . resolve ( * s) )
1315+ . collect :: < Vec < _ > > ( )
1316+ . join ( ", " ) ;
1317+ msg. push_str ( & format ! (
1318+ "\n Could not infer {names} from the goal — supply {} explicitly.\n " ,
1319+ if unsolved. len( ) == 1 { "it" } else { "them" }
1320+ ) ) ;
1321+ elab. err ( msg. trim_end ( ) . to_string ( ) , stmt. span ) ;
1322+ return ;
1323+ }
1324+ // Spell the now-ground axiom application in parameter order.
1325+ let arg_strs: Vec < String > = arule
1326+ . params
1327+ . iter ( )
1328+ . filter_map ( |p| match p {
1329+ Param :: Term { name, .. } => {
1330+ full. iter ( ) . find ( |( s, _) | s == name) . map ( |( _, v) | show ( v, & elab. interner ) )
1331+ }
1332+ Param :: Proof { .. } => None ,
1333+ } )
1334+ . collect ( ) ;
1335+ let axiom_name = match & qname. module {
1336+ Some ( m) => format ! ( "{}.{}" , m. text, qname. name. text) ,
1337+ None => qname. name . text . clone ( ) ,
1338+ } ;
1339+ let ground = format ! ( "{axiom_name}({})" , arg_strs. join( ", " ) ) ;
1340+ msg. push_str ( & format ! (
1341+ "\n Inferred `{ground}` from the goal.\n \n Continue with:\n by rewrite({ground})?;\n "
1342+ ) ) ;
1343+ let fix = Fix {
1344+ title : format ! ( "rewrite({ground})?" ) ,
1345+ replacement : format ! ( "by rewrite({ground})?;" ) ,
1346+ span : stmt. span ,
1347+ } ;
1348+ elab. err_with_fixes ( msg. trim_end ( ) . to_string ( ) , stmt. span , vec ! [ fix] ) ;
1349+ return ;
1350+ }
1351+
1352+ // --- Ground mode: the equation is fully concrete → suggest the forward step. ---
1353+ let _ = & rhs_pat; // rhs comes from the instantiated statement below.
1354+ let eq_stmt = rs. nf ( & subst_all ( & arule. conclusion , & concrete_subst) ) ;
1355+ let ( lhs, rhs) = match & eq_stmt {
1356+ Expr :: Eq ( a, b) => ( ( * * a) . clone ( ) , ( * * b) . clone ( ) ) ,
1357+ _ => unreachable ! ( "instantiated conclusion is still an equation" ) ,
11521358 } ;
11531359
11541360 // The sort of both sides (forward's `T : Sort`).
@@ -1172,7 +1378,6 @@ fn report_rewrite_hole(
11721378 } ;
11731379
11741380 // Build the motive by abstracting every occurrence of `lhs` in the goal.
1175- let goal_nf = rs. nf ( goal) ;
11761381 let lhs_nf = rs. nf ( & lhs) ;
11771382 let v = elab. interner . fresh ( "x" ) ;
11781383 let body = subst_occurrences ( & goal_nf, & lhs_nf, & Expr :: Free ( v) ) ;
@@ -1220,22 +1425,25 @@ fn report_rewrite_hole(
12201425 }
12211426 } ;
12221427
1223- // The concrete `by forward(...)` call: `T`, `lhs`, `rhs`, and the motive printed via
1224- // `show` (all valid surface syntax); the axiom spelled exactly as the user wrote it.
1428+ // The concrete `by forward(...)` call, one argument per line: `T`, `lhs`, `rhs`,
1429+ // and the motive printed via `show` (all valid surface syntax); the axiom spelled
1430+ // exactly as the user wrote it.
12251431 let axiom_src = elab
1226- . span_text ( reference . args [ 0 ] . span )
1432+ . span_text ( axiom_arg . span )
12271433 . map ( str:: to_string)
12281434 . unwrap_or_else ( || show ( & eq_stmt, & elab. interner ) ) ;
1229- let call = format ! (
1230- "by forward({}, {}, {}, {}, {})" ,
1231- show( & t_sort, & elab. interner) ,
1232- show( & lhs, & elab. interner) ,
1233- show( & rhs, & elab. interner) ,
1234- axiom_src,
1235- show( & motive, & elab. interner) ,
1435+ let indent = line_indent ( & elab. source , stmt. span . start ) ;
1436+ let call = format_forward_call (
1437+ & indent,
1438+ & [
1439+ show ( & t_sort, & elab. interner ) ,
1440+ show ( & lhs, & elab. interner ) ,
1441+ show ( & rhs, & elab. interner ) ,
1442+ axiom_src,
1443+ show ( & motive, & elab. interner ) ,
1444+ ] ,
12361445 ) ;
12371446
1238- let indent = line_indent ( & elab. source , stmt. span . start ) ;
12391447 let mut fixes: Vec < Fix > = Vec :: new ( ) ;
12401448 match next. first ( ) {
12411449 None => {
@@ -2183,4 +2391,60 @@ mod fix_tests {
21832391 "should explain why: {ds:?}"
21842392 ) ;
21852393 }
2394+
2395+ #[ test]
2396+ fn rewrite_hole_forward_suggestion_is_multiline ( ) {
2397+ // The forward suggestion breaks each argument onto its own line, indented
2398+ // two spaces past the `by`.
2399+ let src = "import core(forward);\n sort T : Sort;\n op a : -> T;\n op b : -> T;\n op f : T -> T;\n axiom ab |- a = b;\n lemma l\n |- f(a) = f(a);\n proof\n by rewrite(ab)?;\n wip;\n " ;
2400+ let ds = diags ( src) ;
2401+ assert_spans_valid ( src, & ds) ;
2402+ let fix = ds
2403+ . iter ( )
2404+ . flat_map ( |d| & d. fixes )
2405+ . find ( |f| f. replacement . starts_with ( "by forward(" ) )
2406+ . expect ( "ground rewrite should offer a `forward` fix" ) ;
2407+ // `by forward(` then each argument on its own 4-space-indented line (the
2408+ // `by` sits at 2 spaces, arguments two further).
2409+ assert ! (
2410+ fix. replacement. starts_with( "by forward(\n T,\n a,\n b,\n ab,\n λ" ) ,
2411+ "arguments should be one-per-line: {:?}" ,
2412+ fix. replacement
2413+ ) ;
2414+ }
2415+
2416+ #[ test]
2417+ fn rewrite_hole_infers_axiom_arguments ( ) {
2418+ // A bare axiom reference: the arguments are solved by matching the axiom's
2419+ // left side `pop(A, push(A, x, s))` against the goal.
2420+ let stack = "import core(forward);\n sort Stack : Sort -> Sort;\n op empty : forall (A : Sort) st -> Stack(A);\n op push : forall (A : Sort) st A * Stack(A) -> Stack(A);\n op pop : forall (A : Sort) st Stack(A) -> Stack(A);\n op top : forall (A : Sort) st Stack(A) -> A;\n axiom pop_push(A : Sort, x : A, s : Stack(A)) |- pop(A, push(A, x, s)) = s;\n lemma one_pop(A : Sort, a b : A)\n |- top(A, pop(A, push(A, a, push(A, b, empty(A))))) = b;\n proof\n by rewrite(pop_push)?;\n wip;\n " ;
2421+ let ds = diags ( stack) ;
2422+ assert_spans_valid ( stack, & ds) ;
2423+ let fix = ds
2424+ . iter ( )
2425+ . flat_map ( |d| & d. fixes )
2426+ . find ( |f| f. replacement . starts_with ( "by rewrite(" ) )
2427+ . expect ( "bare axiom ref should infer its arguments" ) ;
2428+ assert_eq ! (
2429+ fix. replacement,
2430+ "by rewrite(pop_push(A, a, push(A, b, empty(A))))?;"
2431+ ) ;
2432+ }
2433+
2434+ #[ test]
2435+ fn rewrite_hole_infers_from_named_holes ( ) {
2436+ // Explicit `?` holes (some concrete, some inferred) resolve the same way.
2437+ let stack = "import core(forward);\n sort Stack : Sort -> Sort;\n op empty : forall (A : Sort) st -> Stack(A);\n op push : forall (A : Sort) st A * Stack(A) -> Stack(A);\n op pop : forall (A : Sort) st Stack(A) -> Stack(A);\n op top : forall (A : Sort) st Stack(A) -> A;\n axiom pop_push(A : Sort, x : A, s : Stack(A)) |- pop(A, push(A, x, s)) = s;\n lemma one_pop(A : Sort, a b : A)\n |- top(A, pop(A, push(A, a, push(A, b, empty(A))))) = b;\n proof\n by rewrite(pop_push(A, ?x, ?s))?;\n wip;\n " ;
2438+ let ds = diags ( stack) ;
2439+ assert_spans_valid ( stack, & ds) ;
2440+ let fix = ds
2441+ . iter ( )
2442+ . flat_map ( |d| & d. fixes )
2443+ . find ( |f| f. replacement . starts_with ( "by rewrite(" ) )
2444+ . expect ( "named holes should be inferred" ) ;
2445+ assert_eq ! (
2446+ fix. replacement,
2447+ "by rewrite(pop_push(A, a, push(A, b, empty(A))))?;"
2448+ ) ;
2449+ }
21862450}
0 commit comments