git.iko.soy/rowtype-yoga/purescript-yoga-om-layer
on main help sign in
v1.2.0: TopoWire error graph and ref-lifted dependency

- Refactor wireLayers to use TopoWire with FindReady error graph - ASCII dependency graph in wireLayers error messages with Unicode box-drawing - Add ref-lifted dependency

commit8a57b3c2d1d119858b6a15476cc82cf9e776c93c authorMark <mark.eibes@gmail.com> dateTue 17 Feb 2026, 23:55 (7 months ago) parent231eabc
6 files changed+391−85
.spec-results+33−33 compile-fail-tests/WireLayersMissingDep.purs+4−4 compile-fail-tests/WireLayersWrongOrder.purs−17 spago.lock+20−1 spago.yaml+6−1 src/Yoga/Om/Layer.purs+328−29
.spec-results+33−33
@@ -2,231 +2,231 @@
22 [
33 "Automatic Memoization builds the same layer only once when used in two branches",
44 {
5− "timestamp": "1771091609999.0",
5+ "timestamp": "1771460758484.0",
66 "success": true
77 }
88 ],
99 [
1010 "Automatic Memoization memoized bracketLayer acquires and releases only once",
1111 {
12− "timestamp": "1771091609999.0",
12+ "timestamp": "1771460758484.0",
1313 "success": true
1414 }
1515 ],
1616 [
1717 "Automatic Memoization memoized scoped layer runs finalizer only once",
1818 {
19− "timestamp": "1771091609999.0",
19+ "timestamp": "1771460758484.0",
2020 "success": true
2121 }
2222 ],
2323 [
2424 "Composition proves Row.Nub deduplicates shared requirements at type level",
2525 {
26− "timestamp": "1771091609999.0",
26+ "timestamp": "1771460758484.0",
2727 "success": true
2828 }
2929 ],
3030 [
3131 "Composition proves deduplication at runtime via shared Ref",
3232 {
33− "timestamp": "1771091609999.0",
33+ "timestamp": "1771460758484.0",
3434 "success": true
3535 }
3636 ],
3737 [
3838 "Error Channel composes layers with different error types",
3939 {
40− "timestamp": "1771091609999.0",
40+ "timestamp": "1771460758484.0",
4141 "success": true
4242 }
4343 ],
4444 [
4545 "Error Channel propagates errors from the first layer",
4646 {
47− "timestamp": "1771091609999.0",
47+ "timestamp": "1771460758484.0",
4848 "success": true
4949 }
5050 ],
5151 [
5252 "Error Channel propagates errors from the second layer",
5353 {
54− "timestamp": "1771091609999.0",
54+ "timestamp": "1771460758484.0",
5555 "success": true
5656 }
5757 ],
5858 [
5959 "Error Channel propagates errors through vertical composition",
6060 {
61− "timestamp": "1771091609999.0",
61+ "timestamp": "1771460758484.0",
6262 "success": true
6363 }
6464 ],
6565 [
6666 "Error Channel scoped layer error triggers finalizers",
6767 {
68− "timestamp": "1771091609999.0",
68+ "timestamp": "1771460758484.0",
6969 "success": true
7070 }
7171 ],
7272 [
7373 "Error Channel scoped layers with errors run via runScopedWith",
7474 {
75− "timestamp": "1771091609999.0",
75+ "timestamp": "1771460758484.0",
7676 "success": true
7777 }
7878 ],
7979 [
8080 "Functor / Apply / Bind applies two layers",
8181 {
82− "timestamp": "1771091609999.0",
82+ "timestamp": "1771460758484.0",
8383 "success": true
8484 }
8585 ],
8686 [
8787 "Functor / Apply / Bind binds layers sequentially",
8888 {
89− "timestamp": "1771091609999.0",
89+ "timestamp": "1771460758484.0",
9090 "success": true
9191 }
9292 ],
9393 [
9494 "Functor / Apply / Bind maps over layer output",
9595 {
96− "timestamp": "1771091609999.0",
96+ "timestamp": "1771460758484.0",
9797 "success": true
9898 }
9999 ],
100100 [
101101 "Layer recovering does not retry on non-matching errors",
102102 {
103− "timestamp": "1771091609999.0",
103+ "timestamp": "1771460758484.0",
104104 "success": true
105105 }
106106 ],
107107 [
108108 "Layer recovering only retries the specified error in a multi-error layer",
109109 {
110− "timestamp": "1771091609999.0",
110+ "timestamp": "1771460758484.0",
111111 "success": true
112112 }
113113 ],
114114 [
115115 "Layer recovering preserves the error when retries are exhausted",
116116 {
117− "timestamp": "1771091609999.0",
117+ "timestamp": "1771460758484.0",
118118 "success": true
119119 }
120120 ],
121121 [
122122 "Layer recovering retries layer construction on matching errors",
123123 {
124− "timestamp": "1771091609999.0",
124+ "timestamp": "1771460758484.0",
125125 "success": true
126126 }
127127 ],
128128 [
129129 "Layer recovering uses RetryStatus in the check",
130130 {
131− "timestamp": "1771091609999.0",
131+ "timestamp": "1771460758484.0",
132132 "success": true
133133 }
134134 ],
135135 [
136136 "Layer repeating repeats layer construction while condition holds",
137137 {
138− "timestamp": "1771091609999.0",
138+ "timestamp": "1771460758484.0",
139139 "success": true
140140 }
141141 ],
142142 [
143143 "Layer repeating stops when policy exhausted",
144144 {
145− "timestamp": "1771091609999.0",
145+ "timestamp": "1771460758484.0",
146146 "success": true
147147 }
148148 ],
149149 [
150150 "Parallel runs layers in parallel via parApply",
151151 {
152− "timestamp": "1771091609999.0",
152+ "timestamp": "1771460758484.0",
153153 "success": true
154154 }
155155 ],
156156 [
157157 "Scoped Layers combines scoped and non-scoped layers",
158158 {
159− "timestamp": "1771091609999.0",
159+ "timestamp": "1771460758484.0",
160160 "success": true
161161 }
162162 ],
163163 [
164164 "Scoped Layers fresh layer builds twice even when same base layer",
165165 {
166− "timestamp": "1771091609999.0",
166+ "timestamp": "1771460758484.0",
167167 "success": true
168168 }
169169 ],
170170 [
171171 "Scoped Layers runs finalizers in reverse order on success",
172172 {
173− "timestamp": "1771091609999.0",
173+ "timestamp": "1771460758484.0",
174174 "success": true
175175 }
176176 ],
177177 [
178178 "Scoped Layers runs finalizers when the callback throws",
179179 {
180− "timestamp": "1771091609999.0",
180+ "timestamp": "1771460758484.0",
181181 "success": true
182182 }
183183 ],
184184 [
185185 "Scoped Layers threads finalizers through vertical composition (provide)",
186186 {
187− "timestamp": "1771091609999.0",
187+ "timestamp": "1771460758484.0",
188188 "success": true
189189 }
190190 ],
191191 [
192192 "Scoped Layers works with bracketLayer",
193193 {
194− "timestamp": "1771091609999.0",
194+ "timestamp": "1771460758484.0",
195195 "success": true
196196 }
197197 ],
198198 [
199199 "wireLayers memoizes shared dependencies within wireLayers",
200200 {
201− "timestamp": "1771091609999.0",
201+ "timestamp": "1771460758484.0",
202202 "success": true
203203 }
204204 ],
205205 [
206206 "wireLayers wireLayersDebug emits dependency graph as compiler warning",
207207 {
208− "timestamp": "1771091609999.0",
208+ "timestamp": "1771460758484.0",
209209 "success": true
210210 }
211211 ],
212212 [
213213 "wireLayers wires a diamond dependency",
214214 {
215− "timestamp": "1771091609999.0",
215+ "timestamp": "1771460758484.0",
216216 "success": true
217217 }
218218 ],
219219 [
220220 "wireLayers wires a simple linear chain (A -> B -> C)",
221221 {
222− "timestamp": "1771091609999.0",
222+ "timestamp": "1771460758484.0",
223223 "success": true
224224 }
225225 ],
226226 [
227227 "wireLayers wires independent layers with no inter-dependencies",
228228 {
229− "timestamp": "1771091609999.0",
229+ "timestamp": "1771460758484.0",
230230 "success": true
231231 }
232232 ]
compile-fail-tests/WireLayersMissingDep.purs+4−4
@@ -1,4 +1,4 @@
1−-- EXPECT: TypesDoNotUnify
1+-- EXPECT: Dependency not provided!
22module Yoga.Om.Layer.CompileFailTest where
33
44import Prelude
@@ -8,9 +8,9 @@ import Yoga.Om.Layer (OmLayer, Scope, makeLayer, wireLayers)
88a_base :: OmLayer (scope :: Scope) () { base :: String }
99a_base = makeLayer (pure { base: "ok" })
1010
11−b_upper :: OmLayer (scope :: Scope, missing :: Int) () { upper :: String }
11+b_upper :: OmLayer (scope :: Scope, port :: Int) () { upper :: String }
1212b_upper = makeLayer do
13− { missing } <- Om.ask
14− pure { upper: show missing }
13+ { port } <- Om.ask
14+ pure { upper: show port }
1515
1616bad = wireLayers { a_base, b_upper }
compile-fail-tests/WireLayersWrongOrder.pursdeleted−17
@@ -1,17 +0,0 @@
1−-- EXPECT: TypesDoNotUnify
2−module Yoga.Om.Layer.CompileFailTest where
3−
4−import Prelude
5−import Yoga.Om as Om
6−import Yoga.Om.Layer (OmLayer, Scope, makeLayer, wireLayers)
7−
8−-- a_base needs upper, but b_upper provides it — wrong alphabetical order
9−a_needs_upper :: OmLayer (scope :: Scope, upper :: String) () { final :: String }
10−a_needs_upper = makeLayer do
11− { upper } <- Om.ask
12− pure { final: upper <> "!" }
13−
14−b_upper :: OmLayer (scope :: Scope) () { upper :: String }
15−b_upper = makeLayer (pure { upper: "hello" })
16−
17−bad = wireLayers { a_needs_upper, b_upper }
spago.lock+20−1
@@ -44,6 +44,9 @@
4444 {
4545 "record-studio": ">=1.0.4 <2.0.0"
4646 },
47+ {
48+ "ref-lifted": ">=0.0.1 <1.0.0"
49+ },
4750 {
4851 "refs": ">=6.0.0 <7.0.0"
4952 },
@@ -53,6 +56,9 @@
5356 {
5457 "type-equality": ">=4.0.1 <5.0.0"
5558 },
59+ {
60+ "typelevel-prelude": ">=7.0.0 <8.0.0"
61+ },
5662 {
5763 "unsafe-coerce": ">=6.0.0 <7.0.0"
5864 },
@@ -676,7 +682,11 @@
676682 "zipperarray": "2.0.0"
677683 }
678684 },
679− "extra_packages": {}
685+ "extra_packages": {
686+ "ref-lifted": {
687+ "path": "../purescript-ref-lifted"
688+ }
689+ }
680690 },
681691 "packages": {
682692 "aff": {
@@ -1567,6 +1577,15 @@
15671577 "unsafe-coerce"
15681578 ]
15691579 },
1580+ "ref-lifted": {
1581+ "type": "local",
1582+ "path": "../purescript-ref-lifted",
1583+ "dependencies": [
1584+ "effect",
1585+ "prelude",
1586+ "refs"
1587+ ]
1588+ },
15701589 "refs": {
15711590 "type": "registry",
15721591 "version": "6.0.0",
spago.yaml+6−1
@@ -1,11 +1,14 @@
11workspace:
22 packageSet:
33 registry: 73.0.0
4+ extraPackages:
5+ ref-lifted:
6+ path: ../purescript-ref-lifted
47
58package:
69 name: yoga-om-layer
710 publish:
8− version: 1.1.1
11+ version: 1.2.0
912 license: MIT
1013 location:
1114 githubOwner: rowtype-yoga
@@ -24,9 +27,11 @@ package:
2427 - prelude: ">=6.0.2 <7.0.0"
2528 - record: ">=4.0.0 <5.0.0"
2629 - record-studio: ">=1.0.4 <2.0.0"
30+ - ref-lifted: ">=0.0.1 <1.0.0"
2731 - refs: ">=6.0.0 <7.0.0"
2832 - transformers: ">=6.1.0 <7.0.0"
2933 - type-equality: ">=4.0.1 <5.0.0"
34+ - typelevel-prelude: ">=7.0.0 <8.0.0"
3035 - unsafe-coerce: ">=6.0.0 <7.0.0"
3136 - variant: ">=8.0.0 <9.0.0"
3237 - yoga-om: ">=1.4.0 <2.0.0"
src/Yoga/Om/Layer.purs+328−29
@@ -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