@@ -13,6 +13,7 @@ module Yoga.Om.Layer
1313 , runScopedWith
1414 , withScoped
1515 , withScopedWith
16+ , scoped
1617 , combineRequirements
1718 , provide
1819 , (>->)
@@ -20,18 +21,33 @@ module Yoga.Om.Layer
2021 , repeating
2122 , wireLayers
2223 , wireLayersDebug
23− , wireLayersRL
24− , class WireLayersRL
2524 , class FilterScope
2625 , class FindProvider
2726 , class FindProviderMatch
2827 , class HasLabel
2928 , class EmitEdges
29+ , class DashesFor
30+ , class BoxTop, class BoxMid, class BoxBot
31+ , class RenderDepChildNode
32+ , class IsNotProvided
33+ , class HasDirectMissing, class HasDirectMissingOr
34+ , class FindLayerReqs
35+ , class RenderProviderDeps
36+ , class RenderDepTree
37+ , class FirstProvLabel
38+ , class PrintLayerDispatch
3039 , class PrintLayersRL
3140 , class PrintEdgesRL
41+ , class PrintLayersASCII
42+ , class PrintEdgesASCII
43+ , NotProvided
3244 , class CheckAllProvided
3345 , class CheckAllLabelsExist
3446 , class CheckLabelExists
47+ , class TopoWire, topoWire
48+ , class AllLabelsIn
49+ , class FindReady
50+ , class FindReadyDispatch
3551 ) where
3652
3753import Prelude
@@ -60,8 +76,10 @@ import Prim.Row as Row
6076import Type.Proxy (Proxy(..))
6177import Prim.RowList (class RowToList, Cons, Nil, RowList)
6278import Prim.Boolean (True, False)
63−import Prim.Symbol (class Append)
64−import Prim.TypeError (class Fail, class Warn, Above, Doc, Quote, Text)
79+import Prim.Symbol (class Append, class Cons)
80+import Type.Data.Boolean (class And)
81+import Type.RowList (class RowListAppend)
82+import Prim.TypeError (class Fail, class Warn, Above, Beside, Doc, Quote, Text)
6583import Record as Record
6684import Record.Studio (class Keys)
6785import Type.Equality (class TypeEquals)
@@ -352,7 +370,7 @@ withScoped
352370 . OmLayer (scope :: Scope) () (Record prov)
353371 -> (Record prov -> Aff a)
354372 -> Aff a
355−withScoped = withScopedWith { exception: \_ -> pure (unsafeCoerce {}) }
373+withScoped = withScopedWith { exception: throwError }
356374
357375-- | Like `runScoped` but accepts error handlers for layers with typed errors.
358376runScopedWith
@@ -385,6 +403,17 @@ withScopedWith handlers layer callback = bracket acquire release use
385403 release { scope } = closeScope scope
386404 use { prov } = callback prov
387405
406+scoped
407+ :: forall prov a r rl err_ err
408+ . RowToList (exception :: Error -> Aff (Record prov) | r) rl
409+ => VariantMatchCases rl err_ (Aff (Record prov))
410+ => Union err_ () (exception :: Error | err)
411+ => { exception :: Error -> Aff (Record prov) | r }
412+ -> OmLayer (scope :: Scope) err (Record prov)
413+ -> (Record prov -> Aff a)
414+ -> Aff a
415+scoped = withScopedWith
416+
388417-- =============================================================================
389418-- Horizontal composition — combine two layers with automatic deduplication
390419-- =============================================================================
@@ -481,13 +510,82 @@ repeating policy shouldRepeat (OmLayer layerId om) =
481510 (applyAndDelay policy status >>= maybe (pure result) go)
482511 (pure result)
483512
513+
484514-- =============================================================================
485−-- Auto-wiring
515+-- Topological auto-wiring (order-independent)
486516-- =============================================================================
487517
488−class
489− WireLayersRL
490− (rl :: RowList Type)
518+-- | Check if ALL labels in `needles` exist in `haystack`, returning a Boolean.
519+class AllLabelsIn (needles :: RowList Type) (haystack :: RowList Type) (result :: Boolean)
520+ | needles haystack -> result
521+
522+instance AllLabelsIn Nil haystack True
523+
524+instance
525+ ( HasLabel label haystack found
526+ , AllLabelsIn tail haystack tailResult
527+ , And found tailResult result
528+ ) =>
529+ AllLabelsIn (Cons label ty tail) haystack result
530+
531+-- | Dispatch based on whether a layer is ready (deps satisfied).
532+class FindReadyDispatch
533+ (ready :: Boolean)
534+ (sym :: Symbol)
535+ (layer :: Type)
536+ (tail :: RowList Type)
537+ (available :: RowList Type)
538+ (allLayersRL :: RowList Type)
539+ (foundSym :: Symbol)
540+ (foundLayer :: Type)
541+ (rest :: RowList Type)
542+ | ready sym layer tail available allLayersRL -> foundSym foundLayer rest
543+
544+instance FindReadyDispatch True sym layer tail available allLayersRL sym layer tail
545+
546+else instance
547+ FindReady tail available allLayersRL foundSym foundLayer rest' =>
548+ FindReadyDispatch False sym layer tail available allLayersRL foundSym foundLayer (Cons sym layer rest')
549+
550+-- | Scan candidates for the first layer whose requirements are satisfied.
551+class FindReady
552+ (candidates :: RowList Type)
553+ (available :: RowList Type)
554+ (allLayersRL :: RowList Type)
555+ (sym :: Symbol)
556+ (layer :: Type)
557+ (rest :: RowList Type)
558+ | candidates available allLayersRL -> sym layer rest
559+
560+instance
561+ ( PrintLayersASCII allLayersRL graphDoc
562+ , Fail
563+ ( Above (Text "")
564+ ( Above (Text "Dependency not provided!")
565+ ( Above (Text "")
566+ ( Above (Text "No remaining layer has all its dependencies satisfied.")
567+ ( Above (Text "")
568+ ( Above (Text "Dependency graph:") graphDoc
569+ )
570+ )
571+ )
572+ )
573+ )
574+ )
575+ ) =>
576+ FindReady Nil available allLayersRL sym layer rest
577+
578+else instance
579+ ( RowToList req reqRL
580+ , AllLabelsIn reqRL available isReady
581+ , FindReadyDispatch isReady sym (OmLayer req err (Record prov)) tail available allLayersRL foundSym foundLayer rest
582+ ) =>
583+ FindReady (Cons sym (OmLayer req err (Record prov)) tail) available allLayersRL foundSym foundLayer rest
584+
585+-- | Topologically sort and wire layers. Each iteration finds a layer whose
586+-- | dependencies are already satisfied, processes it, and adds its provisions.
587+class TopoWire
588+ (remaining :: RowList Type)
491589 (layers :: Row Type)
492590 (accReq :: Row Type)
493591 (accErr :: Row Type)
@@ -495,20 +593,25 @@ class
495593 (resReq :: Row Type)
496594 (resErr :: Row Type)
497595 (resProv :: Row Type)
498− | rl layers accReq accErr accProv -> resReq resErr resProv
596+ | remaining layers accReq accErr accProv -> resReq resErr resProv
499597 where
500− wireLayersRL
501− :: Proxy rl
598+ topoWire
599+ :: Proxy remaining
502600 -> Record layers
503601 -> OmLayer accReq accErr (Record accProv)
504602 -> OmLayer resReq resErr (Record resProv)
505603
506−instance WireLayersRL Nil layers accReq accErr accProv accReq accErr accProv where
507− wireLayersRL _ _ acc = acc
604+instance TopoWire Nil layers accReq accErr accProv accReq accErr accProv where
605+ topoWire _ _ acc = acc
508606
509−instance
510− ( IsSymbol sym
511− , Row.Cons sym (OmLayer nextReq nextErr (Record nextProv)) _rest layers
607+else instance
608+ ( RowToList layers allLayersRL
609+ , RowToList accProv accProvRL
610+ , RowToList accReq accReqRL
611+ , RowListAppend accProvRL accReqRL availableRL
612+ , FindReady remaining availableRL allLayersRL readySym (OmLayer nextReq nextErr (Record nextProv)) rest
613+ , IsSymbol readySym
614+ , Row.Cons readySym (OmLayer nextReq nextErr (Record nextProv)) _rest layers
512615 -- next's requirements must be subset of (accProv + accReq)
513616 , Union accProv accReq provReqOut
514617 , Nub provReqOut provReqOut
@@ -521,19 +624,18 @@ instance
521624 , Nub errMergedRaw errMerged
522625 , Union accErr _e1 errMerged
523626 , Union nextErr _e2 errMerged
524− -- merge provisions (Nub provMerged provMerged ensures disjoint)
627+ -- merge provisions
525628 , Union accProv nextProv provMerged
526629 , Nub provMerged provMerged
527630 -- recurse
528− , WireLayersRL tail layers accReq errMerged provMerged resReq resErr resProv
631+ , TopoWire rest layers accReq errMerged provMerged resReq resErr resProv
529632 ) =>
530− WireLayersRL (Cons sym (OmLayer nextReq nextErr (Record nextProv)) tail) layers accReq accErr accProv resReq resErr resProv
531− where
532− wireLayersRL _ layers acc =
533− wireLayersRL (Proxy :: Proxy tail) layers combined
633+ TopoWire remaining layers accReq accErr accProv resReq resErr resProv where
634+ topoWire _ layers acc =
635+ topoWire (Proxy :: Proxy rest) layers combined
534636 where
535637 next :: OmLayer nextReq nextErr (Record nextProv)
536− next = Record.get (Proxy :: Proxy sym) layers
638+ next = Record.get (Proxy :: Proxy readySym) layers
537639
538640 combined :: OmLayer accReq errMerged (Record provMerged)
539641 combined = OmLayer (freshId unit) do
@@ -544,11 +646,11 @@ instance
544646wireLayers
545647 :: forall layers rl req err prov
546648 . RowToList layers rl
547− => WireLayersRL rl layers (scope :: Scope) () () req err prov
649+ => TopoWire rl layers (scope :: Scope) () () req err prov
548650 => Record layers
549651 -> OmLayer req err (Record prov)
550652wireLayers layers =
551− wireLayersRL (Proxy :: Proxy rl) layers seed
653+ topoWire (Proxy :: Proxy rl) layers seed
552654 where
553655 seed = pure {} :: OmLayer (scope :: Scope) () (Record ())
554656
@@ -563,10 +665,16 @@ instance FilterScope Nil Nil
563665instance FilterScope (Cons "scope" Scope tail) tail
564666else instance FilterScope tail out => FilterScope (Cons sym ty tail) (Cons sym ty out)
565667
668+-- | Marker symbol for unresolved dependencies.
669+type NotProvided :: Symbol
670+type NotProvided = "???"
671+
566672-- | Given a label, find which layer in the RowList provides it.
567673class FindProvider (label :: Symbol) (rl :: RowList Type) (provider :: Symbol) | label rl -> provider
568674
569−instance
675+instance FindProvider label Nil NotProvided
676+
677+else instance
570678 ( RowToList prov provRL
571679 , HasLabel label provRL hasIt
572680 , FindProviderMatch hasIt label sym (Cons sym (OmLayer req err (Record prov)) tail) provider
@@ -603,7 +711,7 @@ else instance
603711 ) =>
604712 EmitEdges consumer (Cons label ty tail) allLayers (Above (Text line) restDoc)
605713
606−-- | Walk all layers emitting D2 edges.
714+-- | Walk all layers emitting D2 edges (mermaid format for wireLayersDebug).
607715class PrintLayersRL (rl :: RowList Type) (doc :: Doc) | rl -> doc
608716class PrintEdgesRL (rl :: RowList Type) (allLayers :: RowList Type) (doc :: Doc) | rl allLayers -> doc
609717
@@ -620,12 +728,203 @@ else instance
620728 ) =>
621729 PrintEdgesRL (Cons sym (OmLayer req err (Record prov)) tail) allLayers (Above edgeDoc restDoc)
622730
731+-- | Map a symbol to same-length dashes (for box borders).
732+class DashesFor (sym :: Symbol) (dashes :: Symbol) | sym -> dashes
733+
734+instance DashesFor "" ""
735+
736+else instance
737+ ( Cons head tail sym
738+ , DashesFor tail rest
739+ , Append "─" rest dashes
740+ ) =>
741+ DashesFor sym dashes
742+
743+-- | Build box parts from a symbol.
744+class BoxTop (sym :: Symbol) (result :: Symbol) | sym -> result
745+
746+instance
747+ ( DashesFor sym dashes
748+ , Append "╭─" dashes s0
749+ , Append s0 "─╮" result
750+ ) =>
751+ BoxTop sym result
752+
753+class BoxMid (sym :: Symbol) (result :: Symbol) | sym -> result
754+
755+instance
756+ ( Append "│ " sym s0
757+ , Append s0 " │" result
758+ ) =>
759+ BoxMid sym result
760+
761+class BoxBot (sym :: Symbol) (result :: Symbol) | sym -> result
762+
763+instance
764+ ( DashesFor sym dashes
765+ , Append "╰─" dashes s0
766+ , Append s0 "─╯" result
767+ ) =>
768+ BoxBot sym result
769+
770+-- | Render a single dependency child node (dispatches on provided vs missing).
771+class RenderDepChildNode
772+ (isNotProvided :: Boolean) (provider :: Symbol) (label :: Symbol) (ty :: Type)
773+ (prePrefix :: Symbol) (contPrefix :: Symbol) (branchPrefix :: Symbol)
774+ (allLayers :: RowList Type) (doc :: Doc)
775+ | isNotProvided provider label ty prePrefix contPrefix branchPrefix allLayers -> doc
776+
777+-- Missing dep: boxed label, type outside
778+instance
779+ ( BoxTop label topLine, BoxMid label midLine, BoxBot label botLine
780+ , Append prePrefix topLine topFull
781+ , Append branchPrefix midLine midFull
782+ , Append contPrefix botLine botFull
783+ ) =>
784+ RenderDepChildNode True provider label ty prePrefix contPrefix branchPrefix allLayers
785+ (Above (Text topFull) (Above (Beside (Text midFull) (Beside (Text " :: ") (Beside (Quote ty) (Text " <-- ⚠️ not provided")))) (Text botFull)))
786+
787+-- Provided dep: boxed label, then recurse into provider's sub-tree
788+instance
789+ ( BoxTop label topLine, BoxMid label midLine, BoxBot label botLine
790+ , Append prePrefix topLine topFull
791+ , Append branchPrefix midLine midFull
792+ , Append contPrefix botLine botFull
793+ , Append contPrefix " " subTreePrefix
794+ , RenderProviderDeps provider subTreePrefix allLayers subDoc
795+ ) =>
796+ RenderDepChildNode False provider label ty prePrefix contPrefix branchPrefix allLayers
797+ (Above (Text topFull) (Above (Text midFull) (Above (Text botFull) subDoc)))
798+
799+-- | Is this provider the NotProvided marker?
800+class IsNotProvided (provider :: Symbol) (result :: Boolean) | provider -> result
801+instance IsNotProvided NotProvided True
802+else instance IsNotProvided other False
803+
804+-- | Does any requirement in this RowList resolve to NotProvided?
805+class HasDirectMissing (reqRL :: RowList Type) (allLayers :: RowList Type) (result :: Boolean)
806+ | reqRL allLayers -> result
807+
808+instance HasDirectMissing Nil allLayers False
809+
810+else instance
811+ ( FindProvider label allLayers provider
812+ , IsNotProvided provider isMissing
813+ , HasDirectMissingOr isMissing tail allLayers result
814+ ) =>
815+ HasDirectMissing (Cons label ty tail) allLayers result
816+
817+class HasDirectMissingOr (found :: Boolean) (tail :: RowList Type) (allLayers :: RowList Type) (result :: Boolean)
818+ | found tail allLayers -> result
819+
820+instance HasDirectMissingOr True tail allLayers True
821+
822+else instance
823+ HasDirectMissing tail allLayers result =>
824+ HasDirectMissingOr False tail allLayers result
825+
826+-- | Look up a layer's requirements by name.
827+class FindLayerReqs (sym :: Symbol) (allLayers :: RowList Type) (reqRL :: RowList Type)
828+ | sym allLayers -> reqRL
829+
830+instance
831+ RowToList req reqRL =>
832+ FindLayerReqs sym (Cons sym (OmLayer req err (Record prov)) tail) reqRL
833+
834+else instance
835+ FindLayerReqs sym tail reqRL =>
836+ FindLayerReqs sym (Cons other otherLayer tail) reqRL
837+
838+-- | Render a provider's sub-tree (stops at NotProvided or roots).
839+class RenderProviderDeps
840+ (provider :: Symbol) (prefix :: Symbol) (allLayers :: RowList Type) (doc :: Doc)
841+ | provider prefix allLayers -> doc
842+
843+instance RenderProviderDeps NotProvided prefix allLayers (Text "")
844+
845+else instance
846+ ( FindLayerReqs provider allLayers reqRL
847+ , FilterScope reqRL filteredReqRL
848+ , RenderDepTree prefix filteredReqRL allLayers doc
849+ ) =>
850+ RenderProviderDeps provider prefix allLayers doc
851+
852+-- | Render a dependency tree with tree connectors.
853+class RenderDepTree
854+ (prefix :: Symbol) (reqRL :: RowList Type) (allLayers :: RowList Type) (doc :: Doc)
855+ | prefix reqRL allLayers -> doc
856+
857+instance RenderDepTree prefix Nil allLayers (Text "")
858+
859+-- Last child: └───
860+else instance
861+ ( FindProvider label allLayers provider
862+ , IsNotProvided provider isNP
863+ , Append prefix "│ " prePrefix
864+ , Append prefix " " contPrefix
865+ , Append prefix "└───" branchPrefix
866+ , RenderDepChildNode isNP provider label ty prePrefix contPrefix branchPrefix allLayers doc
867+ ) =>
868+ RenderDepTree prefix (Cons label ty Nil) allLayers doc
869+
870+-- Non-last child: ├───
871+else instance
872+ ( FindProvider label allLayers provider
873+ , IsNotProvided provider isNP
874+ , Append prefix "│ " contPrefix
875+ , Append prefix "├───" branchPrefix
876+ , RenderDepChildNode isNP provider label ty contPrefix contPrefix branchPrefix allLayers childDoc
877+ , RenderDepTree prefix tail allLayers restDoc
878+ ) =>
879+ RenderDepTree prefix (Cons label ty tail) allLayers (Above childDoc restDoc)
880+
881+-- | Extract the first label from a RowList.
882+class FirstProvLabel (rl :: RowList Type) (label :: Symbol) | rl -> label
883+instance FirstProvLabel (Cons label ty tail) label
884+
885+-- | Dispatch: skip layers without missing deps, render tree for others.
886+class PrintLayerDispatch
887+ (show :: Boolean) (displayName :: Symbol) (filteredReqRL :: RowList Type)
888+ (tail :: RowList Type) (allLayers :: RowList Type) (doc :: Doc)
889+ | show displayName filteredReqRL tail allLayers -> doc
890+
891+instance
892+ PrintEdgesASCII tail allLayers doc =>
893+ PrintLayerDispatch False displayName filteredReqRL tail allLayers doc
894+
895+else instance
896+ ( BoxTop displayName topLine, BoxMid displayName midLine, BoxBot displayName botLine
897+ , RenderDepTree " " filteredReqRL allLayers treeDoc
898+ , PrintEdgesASCII tail allLayers restDoc
899+ ) =>
900+ PrintLayerDispatch True displayName filteredReqRL tail allLayers
901+ (Above (Text topLine) (Above (Text midLine) (Above (Text botLine) (Above treeDoc (Above (Text "") restDoc)))))
902+
903+-- | Walk all layers emitting ASCII dependency trees (for error messages).
904+class PrintLayersASCII (rl :: RowList Type) (doc :: Doc) | rl -> doc
905+class PrintEdgesASCII (rl :: RowList Type) (allLayers :: RowList Type) (doc :: Doc) | rl allLayers -> doc
906+
907+instance PrintLayersASCII Nil (Text "")
908+else instance PrintEdgesASCII rl rl doc => PrintLayersASCII rl doc
909+
910+instance PrintEdgesASCII Nil allLayers (Text "")
911+
912+else instance
913+ ( RowToList req reqRL
914+ , FilterScope reqRL filteredReqRL
915+ , RowToList prov provRL
916+ , FirstProvLabel provRL displayName
917+ , HasDirectMissing filteredReqRL allLayers hasMissing
918+ , PrintLayerDispatch hasMissing displayName filteredReqRL tail allLayers doc
919+ ) =>
920+ PrintEdgesASCII (Cons sym (OmLayer req err (Record prov)) tail) allLayers doc
921+
623922wireLayersDebug
624923 :: forall layers rl req err prov doc
625924 . RowToList layers rl
626925 => PrintLayersRL rl doc
627926 => Warn (Above (Text "") (Above (Text "%%{init: {\"flowchart\": {\"defaultRenderer\": \"elk\"}} }%%") (Above (Text "flowchart LR") doc)))
628− => WireLayersRL rl layers (scope :: Scope) () () req err prov
927+ => TopoWire rl layers (scope :: Scope) () () req err prov
629928 => Record layers
630929 -> OmLayer req err (Record prov)
631930wireLayersDebug = wireLayers