lispgram 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LAYOUT_LANGUAGE.md +390 -0
- package/LICENSE +21 -0
- package/LISPGRAM_GRAMMAR.md +568 -0
- package/README.md +326 -0
- package/dist/index.js +5648 -0
- package/dist/interaction.js +9 -0
- package/dist/layout.js +6 -0
- package/dist/surface.js +12 -0
- package/package.json +49 -0
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
Here’s a clean **Lispgram surface-language grammar spec** for the user-friendly lispy language that compiles to NCF.
|
|
2
|
+
|
|
3
|
+
## Lispgram grammar
|
|
4
|
+
|
|
5
|
+
Lispgram is a small lispy language with three main surface forms:
|
|
6
|
+
|
|
7
|
+
* **tree forms** for nodes and parent/child structure
|
|
8
|
+
* **arrow forms** for arrows between nodes
|
|
9
|
+
* **layout forms** for non-semantic renderer intent
|
|
10
|
+
|
|
11
|
+
A document may be either:
|
|
12
|
+
|
|
13
|
+
* a sequence of forms, or
|
|
14
|
+
* wrapped in a top-level `(lispgram ...)`
|
|
15
|
+
|
|
16
|
+
Both shapes are accepted by `parseDocument(source)` in `src/layers/01-surface/parse.js`.
|
|
17
|
+
|
|
18
|
+
Important: label/field braces are a **suffix on the same token** as the id.
|
|
19
|
+
|
|
20
|
+
- ✅ `(somenode{"hi"})`
|
|
21
|
+
- ❌ `(somenode {"hi"})`
|
|
22
|
+
|
|
23
|
+
Whitespace between the id and `{...}` splits them into separate forms/tokens and is not valid attributed-ref syntax.
|
|
24
|
+
|
|
25
|
+
## EBNF
|
|
26
|
+
|
|
27
|
+
```ebnf
|
|
28
|
+
document = { ws_or_comment , form } , ws_or_comment
|
|
29
|
+
| "(" , "lispgram" , { ws_or_comment , form } , ws_or_comment , ")" ;
|
|
30
|
+
|
|
31
|
+
form = tree_form | arrow_form | layout_form ;
|
|
32
|
+
|
|
33
|
+
tree_form = "(" , ws ,
|
|
34
|
+
node_ref ,
|
|
35
|
+
{ ws , tree_item } ,
|
|
36
|
+
ws , ")" ;
|
|
37
|
+
|
|
38
|
+
tree_item = node_ref | tree_form ;
|
|
39
|
+
|
|
40
|
+
arrow_form = "(" , ws ,
|
|
41
|
+
"->" , ws ,
|
|
42
|
+
edge_ref , ws ,
|
|
43
|
+
endpoint , ws ,
|
|
44
|
+
endpoint ,
|
|
45
|
+
ws , ")" ;
|
|
46
|
+
|
|
47
|
+
endpoint = node_ref | vector ;
|
|
48
|
+
|
|
49
|
+
layout_form = "(" , ws , ("$layout" | "$metalayout") ,
|
|
50
|
+
{ ws , s_expression } ,
|
|
51
|
+
ws , ")" ;
|
|
52
|
+
|
|
53
|
+
vector = "[" , ws ,
|
|
54
|
+
node_ref ,
|
|
55
|
+
{ ws , node_ref } ,
|
|
56
|
+
ws , "]" ;
|
|
57
|
+
|
|
58
|
+
node_ref = atom_ref | string_ref | attributed_ref ;
|
|
59
|
+
edge_ref = atom_ref | string_ref | attributed_ref ;
|
|
60
|
+
|
|
61
|
+
attributed_ref
|
|
62
|
+
= atom_ref , "{" , label_text , "}"
|
|
63
|
+
| atom_ref , "{" , label_text , field_map , "}"
|
|
64
|
+
| atom_ref , "{" , field_map , "}" ;
|
|
65
|
+
(* no whitespace is allowed between atom_ref and the opening "{" *)
|
|
66
|
+
|
|
67
|
+
field_map = "{" , field_pair , { ws , field_pair } , "}" ;
|
|
68
|
+
field_pair = field_key , ws , field_value ;
|
|
69
|
+
field_key = field_value ;
|
|
70
|
+
field_value = quoted_string | number | boolean | atom_ref ;
|
|
71
|
+
|
|
72
|
+
label_text = quoted_string | bare_label ;
|
|
73
|
+
|
|
74
|
+
atom_ref = atom_token ;
|
|
75
|
+
string_ref = quoted_string ;
|
|
76
|
+
atom_token = atom_char , { atom_char } ;
|
|
77
|
+
atom_char = any_char_except_ws_or_paren_or_bracket_or_brace ;
|
|
78
|
+
|
|
79
|
+
bare_label = { any_char_except_brace_or_newline } ;
|
|
80
|
+
number = integer | float ;
|
|
81
|
+
boolean = "true" | "false" ;
|
|
82
|
+
|
|
83
|
+
quoted_string = '"' , { string_char } , '"' ;
|
|
84
|
+
|
|
85
|
+
string_char = any_char_except_quote_or_backslash
|
|
86
|
+
| "\" , '"'
|
|
87
|
+
| "\" , "\"
|
|
88
|
+
| "\" , "n"
|
|
89
|
+
| "\" , "r"
|
|
90
|
+
| "\" , "t" ;
|
|
91
|
+
|
|
92
|
+
ws = { " " | "\t" | "\r" | "\n" } ;
|
|
93
|
+
comment = ";" , { any_char_except_newline } ;
|
|
94
|
+
ws_or_comment = ws | comment ;
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
### Layout metadata
|
|
101
|
+
|
|
102
|
+
```lispgram
|
|
103
|
+
($layout
|
|
104
|
+
(same-row [A B])
|
|
105
|
+
(view product :product P :factors [A B] :test-object X)
|
|
106
|
+
(attach-at-boundary h :from Ω :to P :boundary C :side left)
|
|
107
|
+
(parallel-to-boundary q :from M :to N :boundary C :side left))
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Self-loops are not authored as layout directives. Declare an ordinary arrow whose source and target are the same, e.g. `(-> loopP P P)`, and the route layer will autodetect a loop.
|
|
111
|
+
|
|
112
|
+
`$layout` and `$metalayout` are surface metadata. They are preserved through NCF as top-level `(layout ...)` forms, are intentionally omitted from the official semantic core, and lower into the constraint-layout engine when rendered.
|
|
113
|
+
|
|
114
|
+
`$metalayout` remains accepted for older arrow-like layout clauses such as `(-> $samerow [A B C])`; new work should prefer `$layout` relative constraints and template views.
|
|
115
|
+
|
|
116
|
+
## Meaning of each form
|
|
117
|
+
|
|
118
|
+
### 1. Node declaration
|
|
119
|
+
|
|
120
|
+
```lispgram
|
|
121
|
+
(P)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Ensures node `P` exists.
|
|
125
|
+
|
|
126
|
+
Default label is the node id, so this behaves like:
|
|
127
|
+
|
|
128
|
+
* id = `P`
|
|
129
|
+
* label = `P`
|
|
130
|
+
|
|
131
|
+
Example:
|
|
132
|
+
|
|
133
|
+
```lispgram
|
|
134
|
+
(A)
|
|
135
|
+
(B)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Creates two nodes: `A`, `B`.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
### 2. Node with label
|
|
143
|
+
|
|
144
|
+
```lispgram
|
|
145
|
+
(P{hi})
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Ensures node `P` exists with label `hi`.
|
|
149
|
+
|
|
150
|
+
Example:
|
|
151
|
+
|
|
152
|
+
```lispgram
|
|
153
|
+
(P{Person})
|
|
154
|
+
(Q{"Hello world"})
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Creates:
|
|
158
|
+
|
|
159
|
+
* node `P` labeled `Person`
|
|
160
|
+
* node `Q` labeled `Hello world`
|
|
161
|
+
|
|
162
|
+
Use quotes when the label contains spaces.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
### 3. Parent with children
|
|
167
|
+
|
|
168
|
+
```lispgram
|
|
169
|
+
(X Y Z)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Means:
|
|
173
|
+
|
|
174
|
+
* `X` exists
|
|
175
|
+
* `Y` exists and is a child of `X`
|
|
176
|
+
* `Z` exists and is a child of `X`
|
|
177
|
+
|
|
178
|
+
Example:
|
|
179
|
+
|
|
180
|
+
```lispgram
|
|
181
|
+
(Folder File1 File2)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`Folder` is the container, `File1` and `File2` are inside it.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
### 4. Nested parent structure
|
|
189
|
+
|
|
190
|
+
```lispgram
|
|
191
|
+
(X Y (Z P))
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Means:
|
|
195
|
+
|
|
196
|
+
* `Y` is a child of `X`
|
|
197
|
+
* `Z` is a child of `X`
|
|
198
|
+
* `P` is a child of `Z`
|
|
199
|
+
|
|
200
|
+
Example:
|
|
201
|
+
|
|
202
|
+
```lispgram
|
|
203
|
+
(App Header (Body Sidebar))
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
This gives:
|
|
207
|
+
|
|
208
|
+
* `Header` inside `App`
|
|
209
|
+
* `Body` inside `App`
|
|
210
|
+
* `Sidebar` inside `Body`
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
### 5. Chained nesting
|
|
215
|
+
|
|
216
|
+
```lispgram
|
|
217
|
+
(X (Y (Z P)))
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Means:
|
|
221
|
+
|
|
222
|
+
* `Y` is a child of `X`
|
|
223
|
+
* `Z` is a child of `Y`
|
|
224
|
+
* `P` is a child of `Z`
|
|
225
|
+
|
|
226
|
+
Example:
|
|
227
|
+
|
|
228
|
+
```lispgram
|
|
229
|
+
(World (Continent (Country City)))
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
This creates a straight nesting chain.
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
### 6. Arrow between two nodes
|
|
237
|
+
|
|
238
|
+
```lispgram
|
|
239
|
+
(-> e A B)
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Creates arrow `e` from `A` to `B`.
|
|
243
|
+
|
|
244
|
+
If `A` or `B` do not already exist, they are created automatically.
|
|
245
|
+
|
|
246
|
+
Example:
|
|
247
|
+
|
|
248
|
+
```lispgram
|
|
249
|
+
(-> f Login Dashboard)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Creates:
|
|
253
|
+
|
|
254
|
+
* node `Login`
|
|
255
|
+
* node `Dashboard`
|
|
256
|
+
* arrow `f` from `Login` to `Dashboard`
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
### 7. Arrow with label
|
|
261
|
+
|
|
262
|
+
```lispgram
|
|
263
|
+
(-> e{hi} A B)
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Creates arrow `e` from `A` to `B` with label `hi`.
|
|
267
|
+
|
|
268
|
+
Example:
|
|
269
|
+
|
|
270
|
+
```lispgram
|
|
271
|
+
(-> auth{"sign in"} User Session)
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Creates arrow `auth` labeled `sign in`.
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
### 8. Fan-out arrow
|
|
279
|
+
|
|
280
|
+
```lispgram
|
|
281
|
+
(-> e A [B C D])
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Means one source, many targets.
|
|
285
|
+
|
|
286
|
+
This expands to multiple concrete arrows. Each concrete arrow keeps the visible label `e`; generated carrier ids are compiler-private NCF details, not user-facing labels:
|
|
287
|
+
|
|
288
|
+
* `e` from `A` to `B`
|
|
289
|
+
* `e` from `A` to `C`
|
|
290
|
+
* `e` from `A` to `D`
|
|
291
|
+
|
|
292
|
+
Example:
|
|
293
|
+
|
|
294
|
+
```lispgram
|
|
295
|
+
(-> send Server [Client1 Client2 Client3])
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
### 9. Fan-in arrow
|
|
301
|
+
|
|
302
|
+
```lispgram
|
|
303
|
+
(-> e [A B C] D)
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Means many sources, one target.
|
|
307
|
+
|
|
308
|
+
This expands to multiple concrete arrows. Each concrete arrow keeps the visible label `e`:
|
|
309
|
+
|
|
310
|
+
* `e` from `A` to `D`
|
|
311
|
+
* `e` from `B` to `D`
|
|
312
|
+
* `e` from `C` to `D`
|
|
313
|
+
|
|
314
|
+
Example:
|
|
315
|
+
|
|
316
|
+
```lispgram
|
|
317
|
+
(-> collect [Sensor1 Sensor2 Sensor3] Database)
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
## Full examples
|
|
323
|
+
|
|
324
|
+
### Minimal document
|
|
325
|
+
|
|
326
|
+
```lispgram
|
|
327
|
+
(A)
|
|
328
|
+
(B)
|
|
329
|
+
(-> f A B)
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### With grouping
|
|
333
|
+
|
|
334
|
+
```lispgram
|
|
335
|
+
(App Header Footer)
|
|
336
|
+
(App (Main Sidebar))
|
|
337
|
+
(-> nav Header Main)
|
|
338
|
+
(-> info Sidebar Footer)
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### With top-level wrapper
|
|
342
|
+
|
|
343
|
+
```lispgram
|
|
344
|
+
(lispgram
|
|
345
|
+
(A{Start})
|
|
346
|
+
(B{End})
|
|
347
|
+
(-> flow A B))
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### Mixed hierarchy and arrows
|
|
351
|
+
|
|
352
|
+
```lispgram
|
|
353
|
+
(lispgram
|
|
354
|
+
(System
|
|
355
|
+
API
|
|
356
|
+
(UI Button Panel)
|
|
357
|
+
(DB Table))
|
|
358
|
+
|
|
359
|
+
(-> req UI API)
|
|
360
|
+
(-> read API DB)
|
|
361
|
+
(-> write API DB)
|
|
362
|
+
(-> click Button API))
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### Fan-out and fan-in
|
|
366
|
+
|
|
367
|
+
```lispgram
|
|
368
|
+
(lispgram
|
|
369
|
+
(Hub)
|
|
370
|
+
(A)
|
|
371
|
+
(B)
|
|
372
|
+
(C)
|
|
373
|
+
(D)
|
|
374
|
+
|
|
375
|
+
(-> out Hub [A B C])
|
|
376
|
+
(-> in [A B C] D))
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## Semantic rules
|
|
382
|
+
|
|
383
|
+
These are the intended compile-time rules.
|
|
384
|
+
|
|
385
|
+
### Auto-creation
|
|
386
|
+
|
|
387
|
+
Any node mentioned in a tree or arrow form is created if it does not already exist.
|
|
388
|
+
|
|
389
|
+
Example:
|
|
390
|
+
|
|
391
|
+
```lispgram
|
|
392
|
+
(-> f X Y)
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
Creates `X` and `Y` automatically.
|
|
396
|
+
|
|
397
|
+
### Labels and fields
|
|
398
|
+
|
|
399
|
+
A node or arrow may be given a label with `{...}`. The label is stored as `data.label`.
|
|
400
|
+
|
|
401
|
+
Example:
|
|
402
|
+
|
|
403
|
+
```lispgram
|
|
404
|
+
(A{Alpha})
|
|
405
|
+
(-> e{"goes to"} A B)
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
A node or arrow may also carry arbitrary data fields. The compact form puts the label first, followed by a field map:
|
|
409
|
+
|
|
410
|
+
```lispgram
|
|
411
|
+
(A{Person { color blue rank 2 active true }})
|
|
412
|
+
(-> e{maps { color purple weight 2 }} A B)
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
The fully explicit form is a field map only. This is equivalent to the compact node example above:
|
|
416
|
+
|
|
417
|
+
```lispgram
|
|
418
|
+
(A{{ label Person color blue rank 2 active true }})
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
Bare atom values are strings, numbers parse as numbers, and `true` / `false` parse as booleans. Quoted strings may be used when values contain spaces:
|
|
422
|
+
|
|
423
|
+
```lispgram
|
|
424
|
+
(A{{ label "Person Account" color "deep blue" rank 2 }})
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
If no label is provided, the id is used as the default label. A later explicit label overrides that earlier implicit id-based label. Labels are display-only; ids are the referenceable names.
|
|
428
|
+
|
|
429
|
+
Example:
|
|
430
|
+
|
|
431
|
+
```lispgram
|
|
432
|
+
(lispgram
|
|
433
|
+
(-> e A B)
|
|
434
|
+
(A{hi}))
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
This gives node `A` the label `hi`, not `A`.
|
|
438
|
+
|
|
439
|
+
This is rejected because the shorthand label conflicts with the explicit `label` field:
|
|
440
|
+
|
|
441
|
+
```lispgram
|
|
442
|
+
(A{Person { label Account }})
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### Unique ids and arrow groups
|
|
446
|
+
|
|
447
|
+
Node ids must be unique among nodes.
|
|
448
|
+
|
|
449
|
+
Arrow ids are referenceable names. Reusing an arrow id creates an implicit arrow group: each occurrence is a distinct concrete arrow with the same visible label. Referencing that repeated id as an endpoint fans over the whole group.
|
|
450
|
+
|
|
451
|
+
```lispgram
|
|
452
|
+
(-> e A B)
|
|
453
|
+
(-> e C D)
|
|
454
|
+
(-> meta e X)
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
The `meta` arrow above expands over both concrete `e` arrows.
|
|
458
|
+
|
|
459
|
+
If you need finer-grained control while keeping the same visible label, use distinct ids with explicit labels:
|
|
460
|
+
|
|
461
|
+
```lispgram
|
|
462
|
+
(-> e1{e} A B)
|
|
463
|
+
(-> e2{e} C D)
|
|
464
|
+
(-> meta e1 X)
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
Here `e1` and `e2` both display as `e`, but `meta` refers only to `e1`.
|
|
468
|
+
|
|
469
|
+
### Parenting
|
|
470
|
+
|
|
471
|
+
A node may have at most one parent.
|
|
472
|
+
|
|
473
|
+
This is valid:
|
|
474
|
+
|
|
475
|
+
```lispgram
|
|
476
|
+
(X Y)
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
This should be rejected:
|
|
480
|
+
|
|
481
|
+
```lispgram
|
|
482
|
+
(X Y)
|
|
483
|
+
(Z Y)
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
because `Y` would have two parents.
|
|
487
|
+
|
|
488
|
+
### Repeated references
|
|
489
|
+
|
|
490
|
+
Referring to the same node multiple times is allowed.
|
|
491
|
+
|
|
492
|
+
Example:
|
|
493
|
+
|
|
494
|
+
```lispgram
|
|
495
|
+
(A)
|
|
496
|
+
(-> f A B)
|
|
497
|
+
(-> g B A)
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### Conflicting labels and fields
|
|
501
|
+
|
|
502
|
+
This should be treated as an error:
|
|
503
|
+
|
|
504
|
+
```lispgram
|
|
505
|
+
(A{One})
|
|
506
|
+
(A{Two})
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
because node `A` gets two different explicit labels. An explicit label may override an earlier implicit id-based label, but two different explicit labels still conflict. The same rule applies to explicit non-label fields, such as `color` or `rank`.
|
|
510
|
+
|
|
511
|
+
---
|
|
512
|
+
|
|
513
|
+
## Lexical notes
|
|
514
|
+
|
|
515
|
+
### Comments
|
|
516
|
+
|
|
517
|
+
A semicolon starts a comment to the end of the line.
|
|
518
|
+
|
|
519
|
+
```lispgram
|
|
520
|
+
; this is a comment
|
|
521
|
+
(A)
|
|
522
|
+
(-> f A B) ; inline comment
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
### Whitespace
|
|
526
|
+
|
|
527
|
+
Whitespace is ignored except as a separator.
|
|
528
|
+
|
|
529
|
+
So these are equivalent:
|
|
530
|
+
|
|
531
|
+
```lispgram
|
|
532
|
+
(A B C)
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
```lispgram
|
|
536
|
+
(
|
|
537
|
+
A
|
|
538
|
+
B
|
|
539
|
+
C
|
|
540
|
+
)
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
### Reserved tokens
|
|
544
|
+
|
|
545
|
+
`->` is reserved only in list-head position where it denotes an arrow form.
|
|
546
|
+
|
|
547
|
+
`lispgram` is special only as an optional top-level wrapper head. As a plain atom elsewhere, it is parsed like any other identifier.
|
|
548
|
+
|
|
549
|
+
### Labels with spaces
|
|
550
|
+
|
|
551
|
+
Prefer quoted labels when they contain spaces:
|
|
552
|
+
|
|
553
|
+
```lispgram
|
|
554
|
+
(Node{"Hello world"})
|
|
555
|
+
(-> e{"edge label"} A B)
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
---
|
|
559
|
+
|
|
560
|
+
## Practical summary
|
|
561
|
+
|
|
562
|
+
The language has only four ideas:
|
|
563
|
+
|
|
564
|
+
* `(A)` → create a node
|
|
565
|
+
* `(A B C)` → make `B` and `C` children of `A`
|
|
566
|
+
* `(-> e A B)` → create an arrow from `A` to `B`
|
|
567
|
+
* `[A B C]` inside an arrow → fan-in or fan-out
|
|
568
|
+
|