git.iko.soy/rowtype-yoga/purescript-yoga-om-layer
on main help sign in
Add wireLayersDebug with type-level dependency graph printer
commitcace4aefe084b768663bd54a051043a4c0f1f5a0 authorMark <mark.eibes@gmail.com> dateSat 14 Feb 2026, 09:11 (8 months ago) parent15fcbda
3 files changed+104−42
.spec-results+39−32 src/Yoga/Om/Layer.purs+39−9 test/Test/Yoga/Om/Layer/WireLayersSpec.purs+26−1
.spec-results+39−32
@@ -2,224 +2,231 @@
22 [
33 "Automatic Memoization builds the same layer only once when used in two branches",
44 {
5− "timestamp": "1771056676722.0",
5+ "timestamp": "1771057040494.0",
66 "success": true
77 }
88 ],
99 [
1010 "Automatic Memoization memoized bracketLayer acquires and releases only once",
1111 {
12− "timestamp": "1771056676722.0",
12+ "timestamp": "1771057040494.0",
1313 "success": true
1414 }
1515 ],
1616 [
1717 "Automatic Memoization memoized scoped layer runs finalizer only once",
1818 {
19− "timestamp": "1771056676722.0",
19+ "timestamp": "1771057040494.0",
2020 "success": true
2121 }
2222 ],
2323 [
2424 "Composition proves Row.Nub deduplicates shared requirements at type level",
2525 {
26− "timestamp": "1771056676722.0",
26+ "timestamp": "1771057040494.0",
2727 "success": true
2828 }
2929 ],
3030 [
3131 "Composition proves deduplication at runtime via shared Ref",
3232 {
33− "timestamp": "1771056676722.0",
33+ "timestamp": "1771057040494.0",
3434 "success": true
3535 }
3636 ],
3737 [
3838 "Error Channel composes layers with different error types",
3939 {
40− "timestamp": "1771056676722.0",
40+ "timestamp": "1771057040494.0",
4141 "success": true
4242 }
4343 ],
4444 [
4545 "Error Channel propagates errors from the first layer",
4646 {
47− "timestamp": "1771056676722.0",
47+ "timestamp": "1771057040494.0",
4848 "success": true
4949 }
5050 ],
5151 [
5252 "Error Channel propagates errors from the second layer",
5353 {
54− "timestamp": "1771056676722.0",
54+ "timestamp": "1771057040494.0",
5555 "success": true
5656 }
5757 ],
5858 [
5959 "Error Channel propagates errors through vertical composition",
6060 {
61− "timestamp": "1771056676722.0",
61+ "timestamp": "1771057040494.0",
6262 "success": true
6363 }
6464 ],
6565 [
6666 "Error Channel scoped layer error triggers finalizers",
6767 {
68− "timestamp": "1771056676722.0",
68+ "timestamp": "1771057040494.0",
6969 "success": true
7070 }
7171 ],
7272 [
7373 "Error Channel scoped layers with errors run via runScopedWith",
7474 {
75− "timestamp": "1771056676722.0",
75+ "timestamp": "1771057040494.0",
7676 "success": true
7777 }
7878 ],
7979 [
8080 "Functor / Apply / Bind applies two layers",
8181 {
82− "timestamp": "1771056676722.0",
82+ "timestamp": "1771057040494.0",
8383 "success": true
8484 }
8585 ],
8686 [
8787 "Functor / Apply / Bind binds layers sequentially",
8888 {
89− "timestamp": "1771056676722.0",
89+ "timestamp": "1771057040494.0",
9090 "success": true
9191 }
9292 ],
9393 [
9494 "Functor / Apply / Bind maps over layer output",
9595 {
96− "timestamp": "1771056676722.0",
96+ "timestamp": "1771057040494.0",
9797 "success": true
9898 }
9999 ],
100100 [
101101 "Layer recovering does not retry on non-matching errors",
102102 {
103− "timestamp": "1771056676722.0",
103+ "timestamp": "1771057040494.0",
104104 "success": true
105105 }
106106 ],
107107 [
108108 "Layer recovering only retries the specified error in a multi-error layer",
109109 {
110− "timestamp": "1771056676722.0",
110+ "timestamp": "1771057040494.0",
111111 "success": true
112112 }
113113 ],
114114 [
115115 "Layer recovering preserves the error when retries are exhausted",
116116 {
117− "timestamp": "1771056676722.0",
117+ "timestamp": "1771057040494.0",
118118 "success": true
119119 }
120120 ],
121121 [
122122 "Layer recovering retries layer construction on matching errors",
123123 {
124− "timestamp": "1771056676722.0",
124+ "timestamp": "1771057040494.0",
125125 "success": true
126126 }
127127 ],
128128 [
129129 "Layer recovering uses RetryStatus in the check",
130130 {
131− "timestamp": "1771056676722.0",
131+ "timestamp": "1771057040494.0",
132132 "success": true
133133 }
134134 ],
135135 [
136136 "Layer repeating repeats layer construction while condition holds",
137137 {
138− "timestamp": "1771056676722.0",
138+ "timestamp": "1771057040494.0",
139139 "success": true
140140 }
141141 ],
142142 [
143143 "Layer repeating stops when policy exhausted",
144144 {
145− "timestamp": "1771056676722.0",
145+ "timestamp": "1771057040494.0",
146146 "success": true
147147 }
148148 ],
149149 [
150150 "Parallel runs layers in parallel via parApply",
151151 {
152− "timestamp": "1771056676722.0",
152+ "timestamp": "1771057040494.0",
153153 "success": true
154154 }
155155 ],
156156 [
157157 "Scoped Layers combines scoped and non-scoped layers",
158158 {
159− "timestamp": "1771056676722.0",
159+ "timestamp": "1771057040494.0",
160160 "success": true
161161 }
162162 ],
163163 [
164164 "Scoped Layers fresh layer builds twice even when same base layer",
165165 {
166− "timestamp": "1771056676722.0",
166+ "timestamp": "1771057040494.0",
167167 "success": true
168168 }
169169 ],
170170 [
171171 "Scoped Layers runs finalizers in reverse order on success",
172172 {
173− "timestamp": "1771056676722.0",
173+ "timestamp": "1771057040494.0",
174174 "success": true
175175 }
176176 ],
177177 [
178178 "Scoped Layers runs finalizers when the callback throws",
179179 {
180− "timestamp": "1771056676722.0",
180+ "timestamp": "1771057040494.0",
181181 "success": true
182182 }
183183 ],
184184 [
185185 "Scoped Layers threads finalizers through vertical composition (provide)",
186186 {
187− "timestamp": "1771056676722.0",
187+ "timestamp": "1771057040494.0",
188188 "success": true
189189 }
190190 ],
191191 [
192192 "Scoped Layers works with bracketLayer",
193193 {
194− "timestamp": "1771056676722.0",
194+ "timestamp": "1771057040494.0",
195195 "success": true
196196 }
197197 ],
198198 [
199199 "wireLayers memoizes shared dependencies within wireLayers",
200200 {
201− "timestamp": "1771056676722.0",
201+ "timestamp": "1771057040494.0",
202+ "success": true
203+ }
204+ ],
205+ [
206+ "wireLayers wireLayersDebug emits dependency graph as compiler warning",
207+ {
208+ "timestamp": "1771057040494.0",
202209 "success": true
203210 }
204211 ],
205212 [
206213 "wireLayers wires a diamond dependency",
207214 {
208− "timestamp": "1771056676722.0",
215+ "timestamp": "1771057040494.0",
209216 "success": true
210217 }
211218 ],
212219 [
213220 "wireLayers wires a simple linear chain (A -> B -> C)",
214221 {
215− "timestamp": "1771056676722.0",
222+ "timestamp": "1771057040494.0",
216223 "success": true
217224 }
218225 ],
219226 [
220227 "wireLayers wires independent layers with no inter-dependencies",
221228 {
222− "timestamp": "1771056676722.0",
229+ "timestamp": "1771057040494.0",
223230 "success": true
224231 }
225232 ]
src/Yoga/Om/Layer.purs+39−9
@@ -23,6 +23,8 @@ module Yoga.Om.Layer
2323 , wireLayersRL
2424 , class WireLayersRL
2525 , class RowLabels
26+ , class FilterScope
27+ , class FormatLayer
2628 , class PrintLayersRL
2729 , class CheckAllProvided
2830 , class CheckAllLabelsExist
@@ -550,6 +552,7 @@ wireLayers layers =
550552-- Debug graph printing
551553-- =============================================================================
552554
555+-- | Collect labels from a RowList into a comma-separated Symbol.
553556class RowLabels (rl :: RowList Type) (out :: Symbol) | rl -> out
554557
555558instance RowLabels Nil ""
@@ -564,6 +567,39 @@ else instance
564567 ) =>
565568 RowLabels (Cons sym ty tail) out
566569
570+-- | Remove `scope :: Scope` from a RowList (it's always present, just noise).
571+class FilterScope (rl :: RowList Type) (out :: RowList Type) | rl -> out
572+
573+instance FilterScope Nil Nil
574+instance FilterScope (Cons "scope" Scope tail) tail
575+else instance FilterScope tail out => FilterScope (Cons sym ty tail) (Cons sym ty out)
576+
577+-- | Format a single layer line with dep arrows.
578+class FormatLayer (sym :: Symbol) (reqRL :: RowList Type) (provRL :: RowList Type) (line :: Symbol) | sym reqRL provRL -> line
579+
580+-- No deps (after filtering scope)
581+instance
582+ ( RowLabels provRL provSym
583+ , Append " " sym s1
584+ , Append s1 " ── {" s2
585+ , Append s2 provSym s3
586+ , Append s3 "}" line
587+ ) =>
588+ FormatLayer sym Nil provRL line
589+
590+-- Has deps
591+else instance
592+ ( RowLabels reqRL reqSym
593+ , RowLabels provRL provSym
594+ , Append " " sym s1
595+ , Append s1 " ── {" s2
596+ , Append s2 provSym s3
597+ , Append s3 "} ◀ " s4
598+ , Append s4 reqSym line
599+ ) =>
600+ FormatLayer sym reqRL provRL line
601+
602+-- | Walk the layers RowList and build a Doc.
567603class PrintLayersRL (rl :: RowList Type) (doc :: Doc) | rl -> doc
568604
569605instance PrintLayersRL Nil (Text "")
@@ -571,15 +607,9 @@ instance PrintLayersRL Nil (Text "")
571607else instance
572608 ( IsSymbol sym
573609 , RowToList req reqRL
574− , RowLabels reqRL reqSym
610+ , FilterScope reqRL filteredReqRL
575611 , RowToList prov provRL
576− , RowLabels provRL provSym
577− , Append " " sym indent
578− , Append indent " : (" withParen
579− , Append withParen reqSym withReq
580− , Append withReq ") -> {" withArrow
581− , Append withArrow provSym withProv
582− , Append withProv "}" line
612+ , FormatLayer sym filteredReqRL provRL line
583613 , PrintLayersRL tail restDoc
584614 ) =>
585615 PrintLayersRL (Cons sym (OmLayer req err (Record prov)) tail) (Above (Text line) restDoc)
@@ -588,7 +618,7 @@ wireLayersDebug
588618 :: forall layers rl req err prov doc
589619 . RowToList layers rl
590620 => PrintLayersRL rl doc
591− => Warn (Above (Text "Layer wiring:") doc)
621+ => Warn (Above (Text "") (Above (Text "Layer wiring:") doc))
592622 => WireLayersRL rl layers (scope :: Scope) () () req err prov
593623 => Record layers
594624 -> OmLayer req err (Record prov)
test/Test/Yoga/Om/Layer/WireLayersSpec.purs+26−1
@@ -8,7 +8,7 @@ import Effect.Ref as Ref
88import Test.Spec (Spec, describe, it)
99import Test.Spec.Assertions (shouldEqual)
1010import Yoga.Om as Om
11−import Yoga.Om.Layer (OmLayer, Scope, makeLayer, runScoped, wireLayers)
11+import Yoga.Om.Layer (OmLayer, Scope, makeLayer, runScoped, wireLayers, wireLayersDebug)
1212
1313spec :: Spec Unit
1414spec = describe "wireLayers" do
@@ -81,6 +81,31 @@ spec = describe "wireLayers" do
8181 result.repo `shouldEqual` "postgres-repo"
8282 result.app `shouldEqual` "postgres-cache+postgres-repo"
8383
84+ it "wireLayersDebug emits dependency graph as compiler warning" do
85+ let
86+ a_db :: OmLayer (scope :: Scope) () { db :: String }
87+ a_db = makeLayer (pure { db: "postgres" })
88+
89+ b_cache :: OmLayer (scope :: Scope, db :: String) () { cache :: String }
90+ b_cache = makeLayer do
91+ { db } <- Om.ask
92+ pure { cache: db <> "-cache" }
93+
94+ b_repo :: OmLayer (scope :: Scope, db :: String) () { repo :: String }
95+ b_repo = makeLayer do
96+ { db } <- Om.ask
97+ pure { repo: db <> "-repo" }
98+
99+ d_app :: OmLayer (scope :: Scope, cache :: String, repo :: String) () { app :: String }
100+ d_app = makeLayer do
101+ { cache, repo } <- Om.ask
102+ pure { app: cache <> "+" <> repo }
103+
104+ app = wireLayersDebug { a_db, b_cache, b_repo, d_app }
105+
106+ result <- liftAff $ runScoped app
107+ result.app `shouldEqual` "postgres-cache+postgres-repo"
108+
84109 it "memoizes shared dependencies within wireLayers" do
85110 counter <- liftEffect $ Ref.new 0
86111 let