@@ -652,7 +652,7 @@ impl<M: KernelMode> TypeChecker<'_, M> {
652652 // beta firing — Const-headed terms (e.g. literal recursor loops)
653653 // never pay the closure-wrap + readback overhead.
654654 if matches ! ( f. data( ) , ExprData :: Lam ( ..) ) {
655- cur = self . machine_whnf ( f, & args, & mut fuel) ?;
655+ cur = self . machine_whnf ( f, & args, & mut fuel, flags ) ?;
656656 continue ;
657657 }
658658
@@ -713,14 +713,12 @@ impl<M: KernelMode> TypeChecker<'_, M> {
713713 head : KExpr < M > ,
714714 args : & [ KExpr < M > ] ,
715715 fuel : & mut u32 ,
716+ flags : WhnfFlags ,
716717 ) -> Result < KExpr < M > , TcError < M > > {
717718 let mut head = head;
718719 let mut env: MEnv < M > = MEnv :: empty ( ) ;
719- let mut spine: Vec < Arc < Clo < M > > > = args
720- . iter ( )
721- . rev ( )
722- . map ( |a| Arc :: new ( Clo :: closed ( a. clone ( ) ) ) )
723- . collect ( ) ;
720+ let mut spine: Vec < Arc < Clo < M > > > =
721+ args. iter ( ) . rev ( ) . map ( |a| Arc :: new ( Clo :: closed ( a. clone ( ) ) ) ) . collect ( ) ;
724722
725723 loop {
726724 match head. data ( ) {
@@ -748,9 +746,12 @@ impl<M: KernelMode> TypeChecker<'_, M> {
748746 }
749747 let ty2 = clo_subst ( & mut self . env . intern , ty, & env, 0 ) ;
750748 let body2 = clo_subst ( & mut self . env . intern , body, & env, 1 ) ;
751- return Ok (
752- self . intern ( KExpr :: lam ( name. clone ( ) , bi. clone ( ) , ty2, body2) ) ,
753- ) ;
749+ return Ok ( self . intern ( KExpr :: lam (
750+ name. clone ( ) ,
751+ bi. clone ( ) ,
752+ ty2,
753+ body2,
754+ ) ) ) ;
754755 }
755756 } ,
756757
@@ -783,12 +784,43 @@ impl<M: KernelMode> TypeChecker<'_, M> {
783784 }
784785 } ,
785786
787+ // Closure-iota (Phase B; mirrors IxVM try_iota_c): a recursor
788+ // head consumes the spine LAZILY. On the main ctor-rule path the
789+ // rule RHS re-enters the machine with the params/motives/minors
790+ // and post-major arguments as their ORIGINAL closures (plus the
791+ // ctor's fields wrapped closed) — the rule's Lam-chain betas push
792+ // them straight into an environment, so unselected minors
793+ // (dropped match/Decidable branches) are never substituted and
794+ // never read back. Anything off the main path misses to the
795+ // plain readback exit, whose `try_iota` redoes the major's whnf
796+ // against a warm cache. Gated like the eager path: cheap_rec
797+ // mode never runs full iota from inside the machine.
798+ ExprData :: Const ( id, us, _) => {
799+ if !flags. cheap_rec {
800+ let id = id. clone ( ) ;
801+ let us: Vec < KUniv < M > > = us. to_vec ( ) ;
802+ if let Some ( ( rhs, new_spine) ) =
803+ self . try_iota_clo ( & id, & us, & spine) ?
804+ {
805+ if * fuel == 0 {
806+ return Err ( TcError :: MaxRecDepth ) ;
807+ }
808+ * fuel -= 1 ;
809+ head = rhs;
810+ env = MEnv :: empty ( ) ;
811+ spine = new_spine;
812+ continue ;
813+ }
814+ }
815+ let h = head. clone ( ) ;
816+ return Ok ( self . machine_exit ( h, & spine) ) ;
817+ } ,
818+
786819 // Stuck or dispatch-owned heads (no loose Vars of their own):
787- // exit; the outer loop applies iota / prim dispatch / LDecl zeta
820+ // exit; the outer loop applies prim dispatch / LDecl zeta
788821 // exactly as before.
789822 ExprData :: FVar ( ..)
790823 | ExprData :: Sort ( ..)
791- | ExprData :: Const ( ..)
792824 | ExprData :: Nat ( ..)
793825 | ExprData :: Str ( ..) => {
794826 let h = head. clone ( ) ;
@@ -838,6 +870,133 @@ impl<M: KernelMode> TypeChecker<'_, M> {
838870 cur
839871 }
840872
873+ /// Closure-spine iota for the machine's `Const` exit: the main
874+ /// ctor-rule path of [`Self::try_iota_with_flags`], consuming the
875+ /// machine spine lazily. Returns the level-instantiated rule RHS and
876+ /// the machine spine to re-enter with — the params/motives/minors and
877+ /// post-major arguments ride through as their original closures
878+ /// (never read back; only the major materializes), the ctor's field
879+ /// arguments are wrapped closed.
880+ ///
881+ /// Everything off the main path returns `None`, and the caller falls
882+ /// back to the plain readback exit whose `try_iota` is complete:
883+ /// - K recursors (nullary-ctor synthesis needs infer + def-eq),
884+ /// - `Nat` literal majors (the transient-work discipline must keep
885+ /// literal succ-chains out of the interner and the whnf caches, and
886+ /// the linear-rec/offset shortcuts live on the plain path),
887+ /// - `Str` literal majors (ctor coercion + re-whnf),
888+ /// - struct-eta candidates and genuinely stuck majors.
889+ ///
890+ /// A miss costs one readback of the major closure; the plain path's
891+ /// own whnf of that major then hits a warm cache.
892+ fn try_iota_clo (
893+ & mut self ,
894+ rec_id : & KId < M > ,
895+ rec_us : & [ KUniv < M > ] ,
896+ spine : & [ Arc < Clo < M > > ] ,
897+ ) -> Result < Option < ( KExpr < M > , Vec < Arc < Clo < M > > > ) > , TcError < M > > {
898+ let recr = match self . try_get_const ( rec_id) ? {
899+ Some ( KConst :: Recr {
900+ k,
901+ params,
902+ motives,
903+ minors,
904+ indices,
905+ rules,
906+ lvls,
907+ ..
908+ } ) => {
909+ if k {
910+ return Ok ( None ) ;
911+ }
912+ let major_idx = u64_to_usize :: < M > ( params + motives + minors + indices) ?;
913+ if spine. len ( ) <= major_idx {
914+ return Ok ( None ) ;
915+ }
916+ // H6: level params arity (lean4lean Reduce.lean:76).
917+ if rec_us. len ( ) as u64 != lvls {
918+ return Ok ( None ) ;
919+ }
920+ IotaInfo {
921+ k,
922+ params : u64_to_usize :: < M > ( params) ?,
923+ motives : u64_to_usize :: < M > ( motives) ?,
924+ minors : u64_to_usize :: < M > ( minors) ?,
925+ indices : u64_to_usize :: < M > ( indices) ?,
926+ major_idx,
927+ rules,
928+ lvls,
929+ }
930+ } ,
931+ _ => return Ok ( None ) ,
932+ } ;
933+
934+ // Materialize ONLY the major. `spine` is nearest-LAST, so the i-th
935+ // argument nearest-first sits at `spine[len - 1 - i]`.
936+ let len = spine. len ( ) ;
937+ let major_clo = spine[ len - 1 - recr. major_idx ] . clone ( ) ;
938+ let major = clo_readback ( & mut self . env . intern , & major_clo) ;
939+ // Mirror the eager path's pre- and post-whnf offset cleanups so
940+ // `Nat.add base k` majors expose a `Nat.succ` layer here instead of
941+ // missing to a second full attempt.
942+ let major = match self . cleanup_nat_offset_major ( & major) ? {
943+ Some ( cleaned) => cleaned,
944+ None => major,
945+ } ;
946+ let mut major_whnf = self . whnf ( & major) ?;
947+ if matches ! ( major_whnf. data( ) , ExprData :: Nat ( ..) ) {
948+ return Ok ( None ) ;
949+ }
950+ if let Some ( cleaned) = self . cleanup_nat_offset_major ( & major_whnf) ? {
951+ major_whnf = cleaned;
952+ }
953+ if matches ! ( major_whnf. data( ) , ExprData :: Str ( ..) ) {
954+ return Ok ( None ) ;
955+ }
956+
957+ let ( ctor_head, ctor_args) = collect_app_spine ( & major_whnf) ;
958+ let ctor_id = match ctor_head. data ( ) {
959+ ExprData :: Const ( id, _, _) => id. clone ( ) ,
960+ _ => return Ok ( None ) ,
961+ } ;
962+ let ( cidx, ctor_fields) = match self . try_get_const ( & ctor_id) ? {
963+ Some ( KConst :: Ctor { cidx, fields, .. } ) => {
964+ ( u64_to_usize :: < M > ( cidx) ?, u64_to_usize :: < M > ( fields) ?)
965+ } ,
966+ _ => return Ok ( None ) ,
967+ } ;
968+ if cidx >= recr. rules . len ( ) {
969+ return Ok ( None ) ;
970+ }
971+ // H5: nfields ≤ major args (lean4lean Reduce.lean:75).
972+ if ctor_fields > ctor_args. len ( ) {
973+ return Ok ( None ) ;
974+ }
975+
976+ crate :: perf:: record_iota_histo ( & rec_id. addr ) ;
977+ let rule = & recr. rules [ cidx] ;
978+ let rec_us_vec: Vec < _ > = rec_us. to_vec ( ) ;
979+ let rhs = self . instantiate_univ_params ( & rule. rhs , & rec_us_vec) ?;
980+
981+ // New machine spine, nearest-LAST. Nearest-first the rule sees
982+ // `pmm ++ ctor fields ++ post-major`; in machine order that is the
983+ // post-major prefix of `spine` (indices below the major's slot),
984+ // then the fields reversed, then the pmm suffix of `spine`. The
985+ // index arguments between pmm and the major are dropped, as in the
986+ // eager path.
987+ let pmm_end = recr. params + recr. motives + recr. minors ;
988+ let field_start = ctor_args. len ( ) - ctor_fields;
989+ let post_len = len - 1 - recr. major_idx ;
990+ let mut new_spine: Vec < Arc < Clo < M > > > =
991+ Vec :: with_capacity ( post_len + ctor_fields + pmm_end) ;
992+ new_spine. extend_from_slice ( & spine[ ..post_len] ) ;
993+ for arg in ctor_args[ field_start..] . iter ( ) . rev ( ) {
994+ new_spine. push ( Arc :: new ( Clo :: closed ( arg. clone ( ) ) ) ) ;
995+ }
996+ new_spine. extend_from_slice ( & spine[ len - pmm_end..] ) ;
997+ Ok ( Some ( ( rhs, new_spine) ) )
998+ }
999+
8411000 /// WHNF without delta: whnf_core → proj-app → nat/native/string → quot.
8421001 /// Projection values use full WHNF, preserving the public/full semantics.
8431002 pub fn whnf_no_delta (
@@ -3672,8 +3831,7 @@ mod tests {
36723831 let body = AE :: app ( AE :: var ( 1 , ( ) ) , AE :: var ( 0 , ( ) ) ) ;
36733832 let lam2 = AE :: lam ( ( ) , ( ) , sort0 ( ) , AE :: lam ( ( ) , ( ) , sort0 ( ) , body) ) ;
36743833 let app = AE :: app ( lam2, opaque. clone ( ) ) ;
3675- let expected =
3676- AE :: lam ( ( ) , ( ) , sort0 ( ) , AE :: app ( opaque, AE :: var ( 0 , ( ) ) ) ) ;
3834+ let expected = AE :: lam ( ( ) , ( ) , sort0 ( ) , AE :: app ( opaque, AE :: var ( 0 , ( ) ) ) ) ;
36773835 assert_eq ! ( tc. whnf( & app) . unwrap( ) , expected) ;
36783836 }
36793837
@@ -3702,6 +3860,98 @@ mod tests {
37023860 assert_eq ! ( tc. whnf( & app) . unwrap( ) , opaque) ;
37033861 }
37043862
3863+ #[ test]
3864+ fn whnf_machine_closure_iota_via_beta ( ) {
3865+ use super :: super :: constant:: RecRule ;
3866+ // A beta whose body is a recursor application: the machine's Const
3867+ // exit must take the closure-iota path (rule args ride through as
3868+ // closures) and produce the same result as the eager iota.
3869+ //
3870+ // Unit-like inductive `U` with one ctor `U.mk` (no params/fields)
3871+ // and recursor `U.rec : (motive) (minor) (major) → minor`.
3872+ let u_id = mk_id ( "Test.U" ) ;
3873+ let u_mk_id = mk_id ( "Test.U.mk" ) ;
3874+ let u_rec_id = mk_id ( "Test.U.rec" ) ;
3875+ let mut env = env_with_id ( ) ;
3876+ env. insert (
3877+ u_id. clone ( ) ,
3878+ KConst :: Indc {
3879+ name : ( ) ,
3880+ level_params : ( ) ,
3881+ lvls : 0 ,
3882+ params : 0 ,
3883+ indices : 0 ,
3884+ is_rec : true , // disable struct-eta so the ctor path is the only route
3885+ is_refl : false ,
3886+ is_unsafe : false ,
3887+ nested : 0 ,
3888+ block : u_id. clone ( ) ,
3889+ member_idx : 0 ,
3890+ ty : sort0 ( ) ,
3891+ ctors : vec ! [ u_mk_id. clone( ) ] ,
3892+ lean_all : ( ) ,
3893+ } ,
3894+ ) ;
3895+ env. insert (
3896+ u_mk_id. clone ( ) ,
3897+ KConst :: Ctor {
3898+ name : ( ) ,
3899+ level_params : ( ) ,
3900+ is_unsafe : false ,
3901+ lvls : 0 ,
3902+ induct : u_id. clone ( ) ,
3903+ cidx : 0 ,
3904+ params : 0 ,
3905+ fields : 0 ,
3906+ ty : AE :: cnst ( u_id. clone ( ) , Box :: new ( [ ] ) ) ,
3907+ } ,
3908+ ) ;
3909+ env. insert (
3910+ u_rec_id. clone ( ) ,
3911+ KConst :: Recr {
3912+ name : ( ) ,
3913+ level_params : ( ) ,
3914+ k : false ,
3915+ is_unsafe : false ,
3916+ lvls : 0 ,
3917+ params : 0 ,
3918+ indices : 0 ,
3919+ motives : 1 ,
3920+ minors : 1 ,
3921+ block : u_id. clone ( ) ,
3922+ member_idx : 0 ,
3923+ ty : sort0 ( ) ,
3924+ // rule rhs: λ motive minor. minor
3925+ rules : vec ! [ RecRule {
3926+ ctor: ( ) ,
3927+ fields: 0 ,
3928+ rhs: AE :: lam(
3929+ ( ) ,
3930+ ( ) ,
3931+ sort0( ) ,
3932+ AE :: lam( ( ) , ( ) , sort0( ) , AE :: var( 0 , ( ) ) ) ,
3933+ ) ,
3934+ } ] ,
3935+ lean_all : ( ) ,
3936+ } ,
3937+ ) ;
3938+ let mut tc = TypeChecker :: new ( & mut env) ;
3939+ let rec = AE :: cnst ( u_rec_id, Box :: new ( [ ] ) ) ;
3940+ let mk = AE :: cnst ( u_mk_id, Box :: new ( [ ] ) ) ;
3941+ let opaque = AE :: cnst ( mk_id ( "opaque" ) , Box :: new ( [ ] ) ) ;
3942+ // (λ m. U.rec Sort0 m U.mk #2) opaque
3943+ // → machine beta (m := opaque), Const(U.rec) exit → closure-iota
3944+ // → minor = opaque, post-major arg #3 shifts to #2.
3945+ let body = AE :: app (
3946+ AE :: app ( AE :: app ( AE :: app ( rec, sort0 ( ) ) , AE :: var ( 0 , ( ) ) ) , mk) ,
3947+ AE :: var ( 3 , ( ) ) ,
3948+ ) ;
3949+ let lam = AE :: lam ( ( ) , ( ) , sort0 ( ) , body) ;
3950+ let app = AE :: app ( lam, opaque. clone ( ) ) ;
3951+ let expected = AE :: app ( opaque, AE :: var ( 2 , ( ) ) ) ;
3952+ assert_eq ! ( tc. whnf( & app) . unwrap( ) , expected) ;
3953+ }
3954+
37053955 #[ test]
37063956 fn whnf_machine_chained_beta ( ) {
37073957 let mut env = env_with_id ( ) ;
0 commit comments