packages feed

rzk 0.9.0 → 0.9.1

raw patch · 7 files changed

+245/−94 lines, 7 filesdep ~Globdep ~QuickCheckdep ~aesonPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependency ranges changed: Glob, QuickCheck, aeson, array, bifunctors, bytestring, co-log-core, data-default-class, directory, doctest-parallel, filepath, hspec, hspec-discover, lens, lsp, lsp-types, mtl, optparse-generic, stm, template-haskell, text, with-utf8, yaml

API changes (from Hackage documentation)

+ Language.Rzk.Free.Syntax: flattenBinderApp :: Term -> [Term]
+ Rzk.TypeCheck: Branching :: ElimCost
+ Rzk.TypeCheck: SpineStep :: ElimCost
+ Rzk.TypeCheck: data ElimCost
+ Rzk.TypeCheck: instance GHC.Classes.Eq Rzk.TypeCheck.ElimCost
+ Rzk.TypeCheck: instance GHC.Show.Show Rzk.TypeCheck.ElimCost
- Rzk.TypeCheck: eliminatorsOf :: Eq var => TermT var -> TypeCheck var [TermT var -> TermT var]
+ Rzk.TypeCheck: eliminatorsOf :: Eq var => TermT var -> TypeCheck var [(ElimCost, TermT var -> TermT var)]

Files

ChangeLog.md view
@@ -6,6 +6,30 @@ and this project adheres to the [Haskell Package Versioning Policy](https://pvp.haskell.org/). +## Unreleased++## v0.9.1 — 2026-06-26++Fixed:++- In lenient (`--allow-holes`) mode, an unused `#!rzk uses` variable is now tolerated whenever the section contains a hole, not only when the hole sits in the declaration itself. For example, with `#!rzk #def in-progress uses (P) : U := ?` and a hole-free wrapper `#!rzk #def check uses (P) : U := in-progress`, the wrapper's `#!rzk uses (P)` reads as unused only because `#!rzk in-progress` is incomplete; it no longer masks the hole the player is meant to see. Strict mode (the default, and CI) still reports a genuinely unused `#!rzk uses` (see [#262](https://github.com/rzk-lang/rzk/pull/262)).++- A multi-variable binder in a function type, such as `#!rzk (x y : A) → A`, no longer crashes the checker with `expected a pattern but got x y`. It is now desugared into nested one-variable binders `#!rzk (x : A) → (y : A) → A`, as one would expect. The crash was an uncaught `error` reached during translation, so it took down the playground, the game, and the LSP rather than surfacing as a diagnostic; both `rzk typecheck` and the hole query (`--allow-holes`) are fixed (see [#263](https://github.com/rzk-lang/rzk/pull/263)).++- A hole's hint inventory no longer drops a hypothesis or allow-listed lemma whose elimination spine is long. For a goal `#!rzk B` and a lemma `#!rzk deep : A -> A -> A -> A -> A -> A -> A -> A -> B`, the candidate `#!rzk deep ? ? ? ? ? ? ? ?` was silently omitted, because filling each of the eight arguments with a hole spent one unit of the search-depth bound. Filling a function's arguments is a forced spine step, so it no longer counts against the bound; only the genuinely branching eliminators (Σ/cube projections and `#!rzk idJ`) do. This affects only the candidate hints surfaced to the game and the LSP, not the strict `rzk typecheck` path (see [#261](https://github.com/rzk-lang/rzk/pull/261)).++- In lenient (`--allow-holes`) mode, a hole standing for an entire shape-restricted argument is now reported with its goal instead of failing the enclosing extension-type boundary check. When the hole is the whole point of a shape (e.g. a `#!rzk 2 × 2 | Δ¹×Δ¹` point), the surrounding term can unfold to a `#!rzk recOR` whose faces mention the hole (such as `#!rzk π₁ ? ≤ π₂ ?`). Checking an extension-type boundary then enters one of those faces, and a reduction there can drop the hole from the compared terms while the assumed face still depends on it. The boundary mismatch was reported as `cannot unify term`, even though filling the hole makes the term typecheck. A mismatch found under a tope context that still mentions an unfilled hole is now deferred, just like one whose terms still contain a hole. Strict mode (the default, and CI) still reports the hole as unsolved (see [#267](https://github.com/rzk-lang/rzk/pull/267)).++Packaging:++- Add upper bounds on all dependencies, as Hackage asks for. The bounds are at the next super-major version, so a new GHC's boot libraries (`array`, `bytestring`, `directory`, `mtl`, `template-haskell`, `text`) and the pinned Nix/GHCJS package set both stay in range without a Hackage revision on every minor dependency bump. The loose lower bounds are unchanged, so the GHCJS path still resolves (see [#259](https://github.com/rzk-lang/rzk/issues/259), [#134](https://github.com/rzk-lang/rzk/issues/134)).++CI / infrastructure:++- Upgrade the playground's build toolchain to Vite 7 and `tsx` 4.20, picking up the upstream security fixes, and build it with Node 22 under Nix to match (see [#265](https://github.com/rzk-lang/rzk/pull/265)).++- Fix the playground's `ASSET_URL` so the root (`main`) deployment loads its assets from the correct path.+ ## v0.9.0 — 2026-06-24  Added:
rzk.cabal view
@@ -1,11 +1,11 @@ cabal-version: 2.0 --- This file has been generated from package.yaml by hpack version 0.39.6.+-- This file has been generated from package.yaml by hpack version 0.39.1. -- -- see: https://github.com/sol/hpack  name:           rzk-version:        0.9.0+version:        0.9.1 synopsis:       An experimental proof assistant for synthetic ∞-categories description:    Please see the README on GitHub at <https://github.com/rzk-lang/rzk#readme> category:       Dependent Types@@ -31,6 +31,7 @@     test/typecheck/cases/happy-modal-inv.rzk     test/typecheck/cases/happy-modal-tope-unwrap.rzk     test/typecheck/cases/happy-modal-topes.rzk+    test/typecheck/cases/happy-multivar-binder.rzk     test/typecheck/cases/happy-op-hom-to-hom.rzk     test/typecheck/cases/happy-recbot-term-wellformed.rzk     test/typecheck/cases/happy-recor-guard-exceeds-context.rzk@@ -133,6 +134,7 @@     test/typecheck/cases/happy-modal-inv.expect.yaml     test/typecheck/cases/happy-modal-tope-unwrap.expect.yaml     test/typecheck/cases/happy-modal-topes.expect.yaml+    test/typecheck/cases/happy-multivar-binder.expect.yaml     test/typecheck/cases/happy-op-hom-to-hom.expect.yaml     test/typecheck/cases/happy-recbot-term-wellformed.expect.yaml     test/typecheck/cases/happy-recor-guard-exceeds-context.expect.yaml@@ -264,17 +266,17 @@       DeriveDataTypeable   ghc-options: -Wall -Wcompat -Widentities -Werror=missing-fields -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -optP-Wno-nonportable-include-path   build-depends:-      Glob >=0.9.3-    , aeson >=1.4-    , array >=0.5.3.0+      Glob >=0.9.3 && <1+    , aeson >=1.4 && <3+    , array >=0.5.3.0 && <1     , base >=4.7 && <5-    , bifunctors >=5.5.3-    , bytestring >=0.10.8.2-    , directory >=1.2.7.0-    , mtl >=2.3.1-    , template-haskell >=2.14.0.0-    , text >=1.2.3.1-    , yaml >=0.11.0.0+    , bifunctors >=5.5.3 && <6+    , bytestring >=0.10.8.2 && <1+    , directory >=1.2.7.0 && <2+    , mtl >=2.2.2 && <3+    , template-haskell >=2.14.0.0 && <3+    , text >=1.2.3.1 && <3+    , yaml >=0.11.0.0 && <1   default-language: Haskell2010   if flag(lsp) && !impl(ghcjs)     cpp-options: -DLSP_ENABLED@@ -287,15 +289,15 @@         Language.Rzk.VSCode.Lsp         Language.Rzk.VSCode.Tokenize     build-depends:-        aeson >=2.0.0.0-      , co-log-core >=0.3.2.0-      , data-default-class >=0.1.2.0-      , filepath >=1.4.100.0-      , lens >=5.0.1-      , lsp >=2.4.0.0-      , lsp-types >=2.1.1.0-      , mtl >=2.3.1-      , stm >=2.5.1.0+        aeson >=2.0.0.0 && <3+      , co-log-core >=0.3.2.0 && <1+      , data-default-class >=0.1.2.0 && <1+      , filepath >=1.4.100.0 && <2+      , lens >=5.0.1 && <6+      , lsp >=2.4.0.0 && <3+      , lsp-types >=2.1.1.0 && <3+      , mtl >=2.3.1 && <3+      , stm >=2.5.1.0 && <3  executable rzk   main-is: Main.hs@@ -309,25 +311,25 @@       DeriveDataTypeable   ghc-options: -Wall -Wcompat -Widentities -Werror=missing-fields -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -optP-Wno-nonportable-include-path -threaded -rtsopts -with-rtsopts=-N   build-depends:-      Glob >=0.9.3-    , aeson >=1.4-    , array >=0.5.3.0+      Glob >=0.9.3 && <1+    , aeson >=1.4 && <3+    , array >=0.5.3.0 && <1     , base >=4.7 && <5-    , bifunctors >=5.5.3-    , bytestring >=0.10.8.2-    , directory >=1.2.7.0-    , mtl >=2.3.1+    , bifunctors >=5.5.3 && <6+    , bytestring >=0.10.8.2 && <1+    , directory >=1.2.7.0 && <2+    , mtl >=2.2.2 && <3     , rzk-    , template-haskell >=2.14.0.0-    , text >=1.2.3.1-    , yaml >=0.11.0.0+    , template-haskell >=2.14.0.0 && <3+    , text >=1.2.3.1 && <3+    , yaml >=0.11.0.0 && <1   default-language: Haskell2010   if flag(lsp) && !impl(ghcjs)     cpp-options: -DLSP_ENABLED   if !impl(ghcjs)     build-depends:-        optparse-generic >=1.4.7-      , with-utf8 >=1.0.3.0+        optparse-generic >=1.4.7 && <2+      , with-utf8 >=1.0.3.0 && <2  test-suite doctests   type: exitcode-stdio-1.0@@ -338,20 +340,20 @@       DeriveDataTypeable   ghc-options: -Wall -Wcompat -Widentities -Werror=missing-fields -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -optP-Wno-nonportable-include-path   build-depends:-      Glob >=0.9.3-    , QuickCheck >=2.14-    , aeson >=1.4-    , array >=0.5.3.0+      Glob >=0.9.3 && <1+    , QuickCheck >=2.14 && <3+    , aeson >=1.4 && <3+    , array >=0.5.3.0 && <1     , base >=4.11.0.0 && <5.0-    , bifunctors >=5.5.3-    , bytestring >=0.10.8.2-    , directory >=1.2.7.0-    , doctest-parallel >=0.3-    , mtl >=2.3.1+    , bifunctors >=5.5.3 && <6+    , bytestring >=0.10.8.2 && <1+    , directory >=1.2.7.0 && <2+    , doctest-parallel >=0.3 && <1+    , mtl >=2.2.2 && <3     , rzk-    , template-haskell >=2.14.0.0-    , text >=1.2.3.1-    , yaml >=0.11.0.0+    , template-haskell >=2.14.0.0 && <3+    , text >=1.2.3.1 && <3+    , yaml >=0.11.0.0 && <1   default-language: Haskell2010   if flag(lsp) && !impl(ghcjs)     cpp-options: -DLSP_ENABLED@@ -373,23 +375,23 @@       DeriveDataTypeable   ghc-options: -Wall -Wcompat -Widentities -Werror=missing-fields -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -optP-Wno-nonportable-include-path -threaded -rtsopts -with-rtsopts=-N   build-tool-depends:-      hspec-discover:hspec-discover+      hspec-discover:hspec-discover >=2.7 && <3   build-depends:-      Glob >=0.9.3-    , aeson-    , array >=0.5.3.0+      Glob >=0.9.3 && <1+    , aeson >=1.4 && <3+    , array >=0.5.3.0 && <1     , base >=4.7 && <5-    , bifunctors >=5.5.3-    , bytestring >=0.10.8.2-    , directory >=1.2.7.0-    , filepath-    , hspec-    , hspec-discover-    , mtl >=2.3.1+    , bifunctors >=5.5.3 && <6+    , bytestring >=0.10.8.2 && <1+    , directory >=1.2.7.0 && <2+    , filepath >=1.4 && <2+    , hspec >=2.7 && <3+    , hspec-discover >=2.7 && <3+    , mtl >=2.2.2 && <3     , rzk-    , template-haskell >=2.14.0.0-    , text >=1.2.3.1-    , yaml+    , template-haskell >=2.14.0.0 && <3+    , text >=1.2.3.1 && <3+    , yaml >=0.11.0.0 && <1   default-language: Haskell2010   if flag(lsp) && !impl(ghcjs)     cpp-options: -DLSP_ENABLED
src/Language/Rzk/Free/Syntax.hs view
@@ -356,6 +356,19 @@       Rzk.ReflTermType _loc x tA -> Refl (Just (go x, Just (go tA)))       Rzk.IdJ _loc a b c d e f -> IdJ (go a) (go b) (go c) (go d) (go e) (go f)       Rzk.TypeAsc _loc x t -> TypeAsc (go x) (go t)+      -- A binder may name several variables sharing a type, e.g. (x y : A),+      -- which is parsed as the application spine `x y`. Desugar it into nested+      -- one-variable binders ((x : A) → (y : A) → …) before translating, so the+      -- pattern conversion never sees a juxtaposition. (Shape binders are left+      -- alone: their tope refers to the single bound point.)+      Rzk.TypeFun loc (Rzk.ParamTermType loc' patTerm arg) ret+        | _ : _ : _ <- vars ->+            go (foldr (\v -> Rzk.TypeFun loc (Rzk.ParamTermType loc' v arg)) ret vars)+        where vars = flattenBinderApp patTerm+      Rzk.TypeFun loc (Rzk.ParamTermModalType loc' patTerm mc ty) ret+        | _ : _ : _ <- vars ->+            go (foldr (\v -> Rzk.TypeFun loc (Rzk.ParamTermModalType loc' v mc ty)) ret vars)+        where vars = flattenBinderApp patTerm       Rzk.TypeFun _loc (Rzk.ParamTermModalType _loc' patTerm mc ty) ret ->         let pat = unsafeTermToPattern patTerm             md  = modalColonToTModality mc@@ -489,6 +502,15 @@   Flat  -> Rzk.ModalColonFlat Nothing   Op    -> Rzk.ModalColonOp Nothing   Id    -> Rzk.ModalColonId Nothing++-- | Split a binder term into the individual variables it names. A multi-variable+-- binder like @(x y : A)@ is parsed as the application spine @x y@; this returns+-- @[x, y]@ so each can become its own nested binder. A single binder term (a+-- variable, a pair pattern, …) is returned unchanged as a singleton.+flattenBinderApp :: Rzk.Term -> [Rzk.Term]+flattenBinderApp = \case+  Rzk.App _loc f x -> flattenBinderApp f ++ [x]+  t                -> [t]  unsafeTermToPattern :: Rzk.Term -> Rzk.Pattern unsafeTermToPattern = ttp
src/Rzk/TypeCheck.hs view
@@ -738,29 +738,37 @@ -- -- The search is driven uniformly by the eliminators a (weak head normal) type -- admits (see 'eliminatorsOf'), so adding a new eliminator extends it without--- touching the search. Depth is bounded by 'maxEliminationDepth'.+-- touching the search.+--+-- A Π-application is a forced spine step — there is exactly one way to fill an+-- argument (with a hole) — so it extends the spine for free and does not spend+-- the budget. Only the genuinely branching eliminators (Σ/cube projections and+-- @idJ@, flagged by 'eliminatorsOf') count against 'maxEliminationDepth'. The+-- bound therefore limits real search depth, not argument count, so a lemma that+-- must be applied to many holes is still reached. The free spine terminates+-- because each application strips one Π binder off a finite type. allEliminationsInto   :: Eq var => TermT var -> TermT var -> TypeCheck var [TermT var] allEliminationsInto target = go maxEliminationDepth   where     go depth term = do-      ty   <- typeOf term-      fits <- fitsInto term ty target-      deeper <--        if depth <= 0-          then pure []-          else do-            elims <- eliminatorsOf ty-            concat <$> mapM (\wrap -> go (depth - 1) (wrap term)) elims+      ty    <- typeOf term+      fits  <- fitsInto term ty target+      elims <- eliminatorsOf ty+      let step (SpineStep, wrap) = go depth (wrap term)+          step (Branching, wrap)+            | depth <= 0 = pure []+            | otherwise  = go (depth - 1) (wrap term)+      deeper <- concat <$> mapM step elims       pure ([term | fits] <> deeper) --- | How deep an elimination spine 'allEliminationsInto' will build. A temporary--- fixed bound: seven is enough for the spines seen so far (fully applying a--- five-argument hypothesis and projecting twice), and a larger bound mostly adds--- self-referential spines (a built result applied again). It should be made--- configurable, and likely raised, once there is more evidence of what real--- goals need. The chain only branches at Σ-types, which are shallow, so the--- search stays small.+-- | How many /branching/ eliminators 'allEliminationsInto' will chain. Forced+-- Π-applications are free (see 'allEliminationsInto'), so this bounds only the+-- Σ/cube projections and @idJ@ steps, not the argument count of a spine. A+-- temporary fixed bound: branching is shallow in the goals seen so far (a few+-- projections), and a larger bound mostly adds self-referential spines (a built+-- result eliminated again). It should be made configurable once there is more+-- evidence of what real goals need. maxEliminationDepth :: Int maxEliminationDepth = 7 @@ -784,24 +792,32 @@   censor (const []) $ local structuralHoleUnify     ((unify (Just term) target' ty' >> pure True) `catchError` \_ -> pure False) +-- | Whether eliminating a value spends the search budget. A forced+-- Π-application is a 'SpineStep' — there is one way to fill the argument (with a+-- hole), so 'allEliminationsInto' applies it for free; a 'Branching' eliminator+-- (a Σ/cube projection or @idJ@) costs one against 'maxEliminationDepth'.+data ElimCost = SpineStep | Branching+  deriving (Eq, Show)+ -- | The eliminators a value of the given (weak head normal) type admits, each--- as a function wrapping the eliminated term. A Π-type is eliminated by--- application to a fresh hole; a Σ-type by either projection; an identity type--- by path induction (@idJ@), with the motive and base case left as holes.+-- as a function wrapping the eliminated term, paired with its 'ElimCost'. A+-- Π-type is eliminated by application to a fresh hole (a spine step); a Σ-type+-- by either projection; an identity type by path induction (@idJ@), with the+-- motive and base case left as holes. The projections and @idJ@ branch. -- Anything else admits no simple eliminator.-eliminatorsOf :: Eq var => TermT var -> TypeCheck var [TermT var -> TermT var]+eliminatorsOf :: Eq var => TermT var -> TypeCheck var [(ElimCost, TermT var -> TermT var)] eliminatorsOf ty =   case stripTypeRestrictions ty of     TypeFunT _ty _orig _md param _mtope ret ->-      pure [ \term -> let h = mkHole param in appT (substituteT h ret) term h ]+      pure [ (SpineStep, \term -> let h = mkHole param in appT (substituteT h ret) term h) ]     TypeSigmaT _ty _orig _md a b ->-      pure [ \term -> firstT a term-           , \term -> secondT (substituteT (firstT a term) b) term ]+      pure [ (Branching, \term -> firstT a term)+           , (Branching, \term -> secondT (substituteT (firstT a term) b) term) ]     -- A cube point pair (e.g. a pattern-bound @(t , s) : 2 × 2@) projects to its     -- coordinates; rzk renders those projections back as the pattern names.     CubeProductT _ty a b ->-      pure [ \term -> firstT a term-           , \term -> secondT b term ]+      pure [ (Branching, \term -> firstT a term)+           , (Branching, \term -> secondT b term) ]     -- A path @p : a =_A x@ is eliminated by path induction. The motive     -- @C : (z : A) → (a =_A z) → U@ is always a function, so we introduce it     -- straight away as @\\ b q → ?@ rather than leaving it a bare hole: the spine@@ -825,7 +841,7 @@           d      = mkHole dType           motiveAt y p = appT universeT             (appT (typeFunT (BinderVar Nothing) Id (typeIdT a (Just tA) y) Nothing universeT) c y) p-      pure [ \p -> idJT (motiveAt x p) tA a c d x p ]+      pure [ (Branching, \p -> idJT (motiveAt x p) tA a c d x p) ]     _ -> pure []   where     mkHole t = HoleT TypeInfo{ infoType = t, infoWHNF = Nothing, infoNF = Nothing } Nothing@@ -1501,9 +1517,11 @@ scopeToDecls errs ScopeInfo{..} = do   -- In lenient (hole-checking) mode an as-yet-unfilled hole may still come to   -- use a declared variable, so we tolerate the unused-variable diagnostics-  -- wherever such a hole is present: a hole anywhere in the section for an-  -- unused section assumption, a hole in the declaration itself for an unused-  -- 'uses' variable. Strict mode (the default, and CI) keeps reporting both.+  -- wherever such a hole is present anywhere in the section. This covers both an+  -- unused section assumption and an unused 'uses' variable, and crucially a+  -- hole-free definition whose body refers to an in-progress (hole-bearing) one:+  -- its 'uses' reads as unused only because the referenced definition is+  -- incomplete. Strict mode (the default, and CI) keeps reporting both.   lenient <- not <$> asks holesAreErrors   let sectionHasHole = any (maybe False containsHole . varValue . snd) scopeVars   (decls, errs') <- collectScopeDecls (lenient && sectionHasHole) errs [] scopeVars@@ -1511,8 +1529,7 @@   -- when (null errs) $ do   unusedErrors <- forM decls $ \Decl{..} -> do     let unusedUsedVars = declUsedVars `intersect` map fst scopeVars-        declHasHole    = maybe False containsHole declValue-    if null errs && not (null unusedUsedVars) && not (lenient && declHasHole)+    if null errs && not (null unusedUsedVars) && not (lenient && sectionHasHole)       then do         err <- local (\c -> c { location = declLocation }) $           fromTypeError (TypeErrorUnusedUsedVariables unusedUsedVars declName)@@ -3168,12 +3185,20 @@                   -- unification that would otherwise fail is deferred when either                   -- side still contains an (unfilled) hole — including one nested                   -- in a larger term, e.g. @f ?@ checked against an extension-type-                  -- boundary. 'structuralHoleUnify' turns this off, keeping a-                  -- structural mismatch around a hole an error. Lazy: only runs on-                  -- the failure path.+                  -- boundary. The hole may also sit in the tope context rather+                  -- than the terms: a hole standing for a whole shape point makes+                  -- the enclosing 'recOR' split over hole-dependent faces, and a+                  -- branch reduction can drop the hole from the terms while the+                  -- assumed face (e.g. @π₁ ? ≤ π₂ ?@) still mentions it. Such a+                  -- branch is only entered because the hole is unfilled, so a+                  -- mismatch under it is deferred too. 'structuralHoleUnify' turns+                  -- this off, keeping a structural mismatch around a hole an+                  -- error. Lazy: only runs on the failure path.                   defer <- asks deferHoleMismatches+                  topeContextHasHole <- asks (any (containsHole . tTope) . localTopes)                   let def = unless (expected' == actual') err-                      holePresent = defer && (containsHole expected' || containsHole actual')+                      holePresent = defer &&+                        (containsHole expected' || containsHole actual' || topeContextHasHole)                       err                         | holePresent = return ()                         | otherwise =
test/Rzk/HolesSpec.hs view
@@ -95,6 +95,18 @@       let holes2 = holesOf "#lang rzk-1\n#define q : (A : U) -> A -> A\n  := \\ A a -> ?\n#define r : (A : U) -> A -> A\n  := \\ A a -> ?\n"       length holes2 `shouldBe` 2 +    -- A multi-variable binder (x y : A) is parsed as the application spine+    -- `x y`; it must be desugared into nested binders rather than crash+    -- `unsafeTermToPattern` ("expected a pattern but got x y"). The hole query+    -- then sees the hypothesis as the nested function type.+    it "handles a hole whose context has a multi-variable binder" $ do+      case holesOf "#lang rzk-1\n#assume A : U\n#def foo (k : (x y : A) → A) : A := ?\n" of+        [h] -> do+          show (holeGoal h) `shouldBe` "A"+          map (show . holeEntryType) (holeTermVars h)+            `shouldContain` ["(x : A) → (y : A) → A"]+        hs  -> expectationFailure ("expected exactly one hole, got " <> show (length hs))+     -- A hole whose elaborated term reaches unification (here the `refl` endpoint)     -- must not panic ("unexpected term in UNIFY"); it unifies with anything.     it "handles a hole that flows into unification" $ do@@ -140,6 +152,33 @@         [h] -> show (holeGoal h) `shouldBe` "A"         hs  -> expectationFailure ("expected exactly one hole, got " <> show (length hs)) +    -- A hole standing for a /whole/ shape-restricted argument under an enclosing+    -- extension-type boundary. The argument unfolds to a recOR whose faces+    -- mention the hole (e.g. @π₁ ? ≤ π₂ ?@); checking the boundary enters such a+    -- face and a branch reduction then drops the hole from the compared terms+    -- (the triangle here, @const-pt A c@, discards its point). So the mismatch+    -- (@a@ vs @c@) is hole-free in the terms even though the assumed face still+    -- mentions the hole. The per-term deferral cannot see it; deferring on a+    -- hole-bearing tope context can, so the hole is reported with its shape goal+    -- rather than rejected. (Before the fix this raised TypeErrorUnifyTerms.)+    it "handles a hole for a whole shape argument under an extension boundary" $ do+      let src = "#lang rzk-1\n\+                \#def Δ¹ : 2 → TOPE := \\ t → TOP\n\+                \#def Δ¹×Δ¹ : (2 × 2) → TOPE := \\ (t , s) → TOP ∧ TOP\n\+                \#def unfold (A : U) (tri : (2 × 2) → A) : Δ¹×Δ¹ → A\n\+                \  := \\ (t , s) → recOR (t ≤ s ↦ tri (s , t) , s ≤ t ↦ tri (t , s))\n\+                \#def const-pt (A : U) (c : A) : (2 × 2) → A := \\ (t , s) → c\n\+                \#def hom (A : U) (x y : A) : U := (t : Δ¹) → A [ t ≡ 0₂ ↦ x , t ≡ 1₂ ↦ y ]\n\+                \#def test (A : U) (a b c : A) : hom A a b\n\+                \  := \\ t → unfold A (const-pt A c) ?\n"+      case holesOf src of+        [h] -> do+          show (holeGoal h) `shouldBe` "2 × 2"+          case holeGoalShape h of+            Just (s, tope) -> show tope `shouldBe` ("Δ¹×Δ¹ " <> show s)+            Nothing        -> expectationFailure "expected a shape goal (holeGoalShape)"+        hs  -> expectationFailure ("expected exactly one hole, got " <> show (length hs))+     -- A genuinely completable work-in-progress term is tolerated. The example     -- (the Yoneda game's square-transformation level) feeds an incomplete     -- `codomain-square A is-segal-A a b (f ?) ? ? ? ?` where a value of@@ -369,6 +408,18 @@       flip oneHole (holesWithLemmas ["concat"] src) $ \h ->         (names (holeTermVars h) <> names (holeCubeVars h)) `shouldNotContain` ["concat"] +    -- A lemma fully applied to many arguments is offered: filling a function's+    -- arguments with holes is a forced spine step that does not spend the search+    -- budget, so the eight-argument `deep` reaches the goal even though its spine+    -- is longer than `maxEliminationDepth`. (Regression: argument count used to be+    -- charged against the bound, silently dropping deep lemmas.)+    it "offers a lemma applied to more arguments than maxEliminationDepth" $+      let deepSrc = "#lang rzk-1\n#postulate A : U\n#postulate B : U\n"+                 <> "#postulate deep : A -> A -> A -> A -> A -> A -> A -> A -> B\n"+                 <> "#define goal : B := ?\n"+      in flip oneHole (holesWithLemmas ["deep"] deepSrc) $ \h ->+           cands h `shouldContain` ["deep ? ? ? ? ? ? ? ?"]+   describe "holeIntroductions (type-directed introduction forms)" $ do     let intros = map show . holeIntroductions @@ -475,6 +526,15 @@     it "still reports an unused 'uses' variable with no hole in the declaration" $       errTagsOf (usesSection "#define f uses (x) : U\n  := A")         `shouldContain` ["TypeErrorUnusedUsedVariables"]++    -- A hole-free definition whose body refers to an in-progress (hole-bearing)+    -- one declares 'uses (x)' that reads as unused only because the referenced+    -- definition is incomplete. The section has a hole, so it is tolerated (the+    -- check keys off a hole anywhere in the section, not the declaration alone).+    it "tolerates an unused 'uses' on a hole-free wrapper of a hole-bearing def" $+      errTagsOf (usesSection ("#define in-progress uses (x) : U\n  := ?\n"+                           <> "#define wrapper uses (x) : U\n  := in-progress"))+        `shouldNotContain` ["TypeErrorUnusedUsedVariables"]    -- The goal cell: when the goal is a renderable shape, the hole carries an SVG   -- of that shape, drawn from an abstract inhabitant with the proof term hidden
+ test/typecheck/cases/happy-multivar-binder.expect.yaml view
@@ -0,0 +1,3 @@+status: ok+regression_for:+  - multivar-binder-desugar-unsafeTermToPattern-crash
+ test/typecheck/cases/happy-multivar-binder.rzk view
@@ -0,0 +1,15 @@+#lang rzk-1++-- A multi-variable binder like (x y : A) desugars to nested one-variable+-- binders (x : A) → (y : A) → …, so a function of that type accepts that many+-- arguments. Previously the surface term `x y` crashed `unsafeTermToPattern`.++#define app2 (A : U) (a : A) (k : (x y : A) → A)+  : A+  := k a a++#define app3 (A : U) (a : A) (k : (x y z : A) → A)+  : A+  := k a a a++#check app2 : (A : U) → (a : A) → ((x y : A) → A) → A