jaclang 0.7.22__py3-none-any.whl → 0.7.23__py3-none-any.whl

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.

Potentially problematic release.


This version of jaclang might be problematic. Click here for more details.

@@ -7,12 +7,9 @@ from enum import IntEnum
7
7
  from logging import getLogger
8
8
  from pickle import dumps
9
9
  from types import UnionType
10
- from typing import Any, Callable, ClassVar, Iterable, Optional, TypeVar
10
+ from typing import Any, Callable, ClassVar, Optional, TypeVar
11
11
  from uuid import UUID, uuid4
12
12
 
13
- from jaclang.compiler.constant import EdgeDir
14
- from jaclang.runtimelib.utils import collect_node_connections
15
-
16
13
  logger = getLogger(__name__)
17
14
 
18
15
  TARCH = TypeVar("TARCH", bound="Architype")
@@ -77,128 +74,6 @@ class Anchor:
77
74
  persistent: bool = False
78
75
  hash: int = 0
79
76
 
80
- ##########################################################################
81
- # ACCESS CONTROL: TODO: Make Base Type #
82
- ##########################################################################
83
-
84
- def allow_root(
85
- self, root_id: UUID, level: AccessLevel | int | str = AccessLevel.READ
86
- ) -> None:
87
- """Allow all access from target root graph to current Architype."""
88
- level = AccessLevel.cast(level)
89
- access = self.access.roots
90
-
91
- _root_id = str(root_id)
92
- if level != access.anchors.get(_root_id, AccessLevel.NO_ACCESS):
93
- access.anchors[_root_id] = level
94
-
95
- def disallow_root(
96
- self, root_id: UUID, level: AccessLevel | int | str = AccessLevel.READ
97
- ) -> None:
98
- """Disallow all access from target root graph to current Architype."""
99
- level = AccessLevel.cast(level)
100
- access = self.access.roots
101
-
102
- access.anchors.pop(str(root_id), None)
103
-
104
- def unrestrict(self, level: AccessLevel | int | str = AccessLevel.READ) -> None:
105
- """Allow everyone to access current Architype."""
106
- level = AccessLevel.cast(level)
107
- if level != self.access.all:
108
- self.access.all = level
109
-
110
- def restrict(self) -> None:
111
- """Disallow others to access current Architype."""
112
- if self.access.all > AccessLevel.NO_ACCESS:
113
- self.access.all = AccessLevel.NO_ACCESS
114
-
115
- def has_read_access(self, to: Anchor) -> bool:
116
- """Read Access Validation."""
117
- if not (access_level := self.access_level(to) > AccessLevel.NO_ACCESS):
118
- logger.info(
119
- f"Current root doesn't have read access to {to.__class__.__name__}[{to.id}]"
120
- )
121
- return access_level
122
-
123
- def has_connect_access(self, to: Anchor) -> bool:
124
- """Write Access Validation."""
125
- if not (access_level := self.access_level(to) > AccessLevel.READ):
126
- logger.info(
127
- f"Current root doesn't have connect access to {to.__class__.__name__}[{to.id}]"
128
- )
129
- return access_level
130
-
131
- def has_write_access(self, to: Anchor) -> bool:
132
- """Write Access Validation."""
133
- if not (access_level := self.access_level(to) > AccessLevel.CONNECT):
134
- logger.info(
135
- f"Current root doesn't have write access to {to.__class__.__name__}[{to.id}]"
136
- )
137
- return access_level
138
-
139
- def access_level(self, to: Anchor) -> AccessLevel:
140
- """Access validation."""
141
- if not to.persistent:
142
- return AccessLevel.WRITE
143
-
144
- from jaclang.plugin.feature import JacFeature as Jac
145
-
146
- jctx = Jac.get_context()
147
-
148
- jroot = jctx.root
149
-
150
- # if current root is system_root
151
- # if current root id is equal to target anchor's root id
152
- # if current root is the target anchor
153
- if jroot == jctx.system_root or jroot.id == to.root or jroot == to:
154
- return AccessLevel.WRITE
155
-
156
- access_level = AccessLevel.NO_ACCESS
157
-
158
- # if target anchor have set access.all
159
- if (to_access := to.access).all > AccessLevel.NO_ACCESS:
160
- access_level = to_access.all
161
-
162
- # if target anchor's root have set allowed roots
163
- # if current root is allowed to the whole graph of target anchor's root
164
- if to.root and isinstance(to_root := jctx.mem.find_one(to.root), Anchor):
165
- if to_root.access.all > access_level:
166
- access_level = to_root.access.all
167
-
168
- level = to_root.access.roots.check(str(jroot.id))
169
- if level > AccessLevel.NO_ACCESS and access_level == AccessLevel.NO_ACCESS:
170
- access_level = level
171
-
172
- # if target anchor have set allowed roots
173
- # if current root is allowed to target anchor
174
- level = to_access.roots.check(str(jroot.id))
175
- if level > AccessLevel.NO_ACCESS and access_level == AccessLevel.NO_ACCESS:
176
- access_level = level
177
-
178
- return access_level
179
-
180
- # ---------------------------------------------------------------------- #
181
-
182
- def save(self) -> None:
183
- """Save Anchor."""
184
- from jaclang.plugin.feature import JacFeature as Jac
185
-
186
- jctx = Jac.get_context()
187
-
188
- self.persistent = True
189
- self.root = jctx.root.id
190
-
191
- jctx.mem.set(self.id, self)
192
-
193
- def destroy(self) -> None:
194
- """Destroy Anchor."""
195
- from jaclang.plugin.feature import JacFeature as Jac
196
-
197
- jctx = Jac.get_context()
198
-
199
- if jctx.root.has_write_access(self):
200
- jctx.mem.remove(self.id)
201
-
202
77
  def is_populated(self) -> bool:
203
78
  """Check if state."""
204
79
  return "architype" in self.__dict__
@@ -304,122 +179,6 @@ class NodeAnchor(Anchor):
304
179
  architype: NodeArchitype
305
180
  edges: list[EdgeAnchor]
306
181
 
307
- def get_edges(
308
- self,
309
- dir: EdgeDir,
310
- filter_func: Optional[Callable[[list[EdgeArchitype]], list[EdgeArchitype]]],
311
- target_obj: Optional[list[NodeArchitype]],
312
- ) -> list[EdgeArchitype]:
313
- """Get edges connected to this node."""
314
- from jaclang.plugin.feature import JacFeature as Jac
315
-
316
- root = Jac.get_root().__jac__
317
- ret_edges: list[EdgeArchitype] = []
318
- for anchor in self.edges:
319
- if (
320
- (source := anchor.source)
321
- and (target := anchor.target)
322
- and (not filter_func or filter_func([anchor.architype]))
323
- and source.architype
324
- and target.architype
325
- ):
326
- if (
327
- dir in [EdgeDir.OUT, EdgeDir.ANY]
328
- and self == source
329
- and (not target_obj or target.architype in target_obj)
330
- and root.has_read_access(target)
331
- ):
332
- ret_edges.append(anchor.architype)
333
- if (
334
- dir in [EdgeDir.IN, EdgeDir.ANY]
335
- and self == target
336
- and (not target_obj or source.architype in target_obj)
337
- and root.has_read_access(source)
338
- ):
339
- ret_edges.append(anchor.architype)
340
- return ret_edges
341
-
342
- def edges_to_nodes(
343
- self,
344
- dir: EdgeDir,
345
- filter_func: Optional[Callable[[list[EdgeArchitype]], list[EdgeArchitype]]],
346
- target_obj: Optional[list[NodeArchitype]],
347
- ) -> list[NodeArchitype]:
348
- """Get set of nodes connected to this node."""
349
- from jaclang.plugin.feature import JacFeature as Jac
350
-
351
- root = Jac.get_root().__jac__
352
- ret_edges: list[NodeArchitype] = []
353
- for anchor in self.edges:
354
- if (
355
- (source := anchor.source)
356
- and (target := anchor.target)
357
- and (not filter_func or filter_func([anchor.architype]))
358
- and source.architype
359
- and target.architype
360
- ):
361
- if (
362
- dir in [EdgeDir.OUT, EdgeDir.ANY]
363
- and self == source
364
- and (not target_obj or target.architype in target_obj)
365
- and root.has_read_access(target)
366
- ):
367
- ret_edges.append(target.architype)
368
- if (
369
- dir in [EdgeDir.IN, EdgeDir.ANY]
370
- and self == target
371
- and (not target_obj or source.architype in target_obj)
372
- and root.has_read_access(source)
373
- ):
374
- ret_edges.append(source.architype)
375
- return ret_edges
376
-
377
- def remove_edge(self, edge: EdgeAnchor) -> None:
378
- """Remove reference without checking sync status."""
379
- for idx, ed in enumerate(self.edges):
380
- if ed.id == edge.id:
381
- self.edges.pop(idx)
382
- break
383
-
384
- def gen_dot(self, dot_file: Optional[str] = None) -> str:
385
- """Generate Dot file for visualizing nodes and edges."""
386
- visited_nodes: set[NodeAnchor] = set()
387
- connections: set[tuple[NodeArchitype, NodeArchitype, str]] = set()
388
- unique_node_id_dict = {}
389
-
390
- collect_node_connections(self, visited_nodes, connections)
391
- dot_content = 'digraph {\nnode [style="filled", shape="ellipse", fillcolor="invis", fontcolor="black"];\n'
392
- for idx, i in enumerate([nodes_.architype for nodes_ in visited_nodes]):
393
- unique_node_id_dict[i] = (i.__class__.__name__, str(idx))
394
- dot_content += f'{idx} [label="{i}"];\n'
395
- dot_content += 'edge [color="gray", style="solid"];\n'
396
-
397
- for pair in list(set(connections)):
398
- dot_content += (
399
- f"{unique_node_id_dict[pair[0]][1]} -> {unique_node_id_dict[pair[1]][1]}"
400
- f' [label="{pair[2]}"];\n'
401
- )
402
- if dot_file:
403
- with open(dot_file, "w") as f:
404
- f.write(dot_content + "}")
405
- return dot_content + "}"
406
-
407
- def spawn_call(self, walk: WalkerAnchor) -> WalkerArchitype:
408
- """Invoke data spatial call."""
409
- return walk.spawn_call(self)
410
-
411
- def destroy(self) -> None:
412
- """Destroy Anchor."""
413
- from jaclang.plugin.feature import JacFeature as Jac
414
-
415
- jctx = Jac.get_context()
416
-
417
- if jctx.root.has_write_access(self):
418
- for edge in self.edges:
419
- edge.destroy()
420
-
421
- jctx.mem.remove(self.id)
422
-
423
182
  def __getstate__(self) -> dict[str, object]:
424
183
  """Serialize Node Anchor."""
425
184
  state = super().__getstate__()
@@ -439,30 +198,6 @@ class EdgeAnchor(Anchor):
439
198
  target: NodeAnchor
440
199
  is_undirected: bool
441
200
 
442
- def __post_init__(self) -> None:
443
- """Populate edge to source and target."""
444
- self.source.edges.append(self)
445
- self.target.edges.append(self)
446
-
447
- def detach(self) -> None:
448
- """Detach edge from nodes."""
449
- self.source.remove_edge(self)
450
- self.target.remove_edge(self)
451
-
452
- def spawn_call(self, walk: WalkerAnchor) -> WalkerArchitype:
453
- """Invoke data spatial call."""
454
- return walk.spawn_call(self.target)
455
-
456
- def destroy(self) -> None:
457
- """Destroy Anchor."""
458
- from jaclang.plugin.feature import JacFeature as Jac
459
-
460
- jctx = Jac.get_context()
461
-
462
- if jctx.root.has_write_access(self):
463
- self.detach()
464
- jctx.mem.remove(self.id)
465
-
466
201
  def __getstate__(self) -> dict[str, object]:
467
202
  """Serialize Node Anchor."""
468
203
  state = super().__getstate__()
@@ -489,99 +224,6 @@ class WalkerAnchor(Anchor):
489
224
  ignores: list[Anchor] = field(default_factory=list)
490
225
  disengaged: bool = False
491
226
 
492
- def visit_node(self, anchors: Iterable[NodeAnchor | EdgeAnchor]) -> bool:
493
- """Walker visits node."""
494
- before_len = len(self.next)
495
- for anchor in anchors:
496
- if anchor not in self.ignores:
497
- if isinstance(anchor, NodeAnchor):
498
- self.next.append(anchor)
499
- elif isinstance(anchor, EdgeAnchor):
500
- if target := anchor.target:
501
- self.next.append(target)
502
- else:
503
- raise ValueError("Edge has no target.")
504
- return len(self.next) > before_len
505
-
506
- def ignore_node(self, anchors: Iterable[NodeAnchor | EdgeAnchor]) -> bool:
507
- """Walker ignores node."""
508
- before_len = len(self.ignores)
509
- for anchor in anchors:
510
- if anchor not in self.ignores:
511
- if isinstance(anchor, NodeAnchor):
512
- self.ignores.append(anchor)
513
- elif isinstance(anchor, EdgeAnchor):
514
- if target := anchor.target:
515
- self.ignores.append(target)
516
- else:
517
- raise ValueError("Edge has no target.")
518
- return len(self.ignores) > before_len
519
-
520
- def disengage_now(self) -> None:
521
- """Disengage walker from traversal."""
522
- self.disengaged = True
523
-
524
- def spawn_call(self, node: Anchor) -> WalkerArchitype:
525
- """Invoke data spatial call."""
526
- if walker := self.architype:
527
- self.path = []
528
- self.next = [node]
529
- if self.next:
530
- current_node = self.next[-1].architype
531
- for i in walker._jac_entry_funcs_:
532
- if not i.trigger:
533
- if i.func:
534
- i.func(walker, current_node)
535
- else:
536
- raise ValueError(f"No function {i.name} to call.")
537
- while len(self.next):
538
- if current_node := self.next.pop(0).architype:
539
- for i in current_node._jac_entry_funcs_:
540
- if not i.trigger or isinstance(walker, i.trigger):
541
- if i.func:
542
- i.func(current_node, walker)
543
- else:
544
- raise ValueError(f"No function {i.name} to call.")
545
- if self.disengaged:
546
- return walker
547
- for i in walker._jac_entry_funcs_:
548
- if not i.trigger or isinstance(current_node, i.trigger):
549
- if i.func and i.trigger:
550
- i.func(walker, current_node)
551
- elif not i.trigger:
552
- continue
553
- else:
554
- raise ValueError(f"No function {i.name} to call.")
555
- if self.disengaged:
556
- return walker
557
- for i in walker._jac_exit_funcs_:
558
- if not i.trigger or isinstance(current_node, i.trigger):
559
- if i.func and i.trigger:
560
- i.func(walker, current_node)
561
- elif not i.trigger:
562
- continue
563
- else:
564
- raise ValueError(f"No function {i.name} to call.")
565
- if self.disengaged:
566
- return walker
567
- for i in current_node._jac_exit_funcs_:
568
- if not i.trigger or isinstance(walker, i.trigger):
569
- if i.func:
570
- i.func(current_node, walker)
571
- else:
572
- raise ValueError(f"No function {i.name} to call.")
573
- if self.disengaged:
574
- return walker
575
- for i in walker._jac_exit_funcs_:
576
- if not i.trigger:
577
- if i.func:
578
- i.func(walker, current_node)
579
- else:
580
- raise ValueError(f"No function {i.name} to call.")
581
- self.ignores = []
582
- return walker
583
- raise Exception(f"Invalid Reference {self.id}")
584
-
585
227
 
586
228
  class Architype:
587
229
  """Architype Protocol."""
@@ -613,17 +255,6 @@ class EdgeArchitype(Architype):
613
255
 
614
256
  __jac__: EdgeAnchor
615
257
 
616
- def __attach__(
617
- self,
618
- source: NodeAnchor,
619
- target: NodeAnchor,
620
- is_undirected: bool,
621
- ) -> None:
622
- """Attach EdgeAnchor properly."""
623
- self.__jac__ = EdgeAnchor(
624
- architype=self, source=source, target=target, is_undirected=is_undirected
625
- )
626
-
627
258
 
628
259
  class WalkerArchitype(Architype):
629
260
  """Walker Architype Protocol."""
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
 
6
6
  from .architype import (
7
+ AccessLevel,
7
8
  Anchor,
8
9
  Architype,
9
10
  DSFunc,
@@ -21,6 +22,7 @@ from .memory import Memory, ShelfStorage
21
22
  from .test import JacTestCheck, JacTestResult, JacTextTestRunner
22
23
 
23
24
  __all__ = [
25
+ "AccessLevel",
24
26
  "Anchor",
25
27
  "NodeAnchor",
26
28
  "EdgeAnchor",
@@ -42,10 +42,6 @@ class ExecutionContext:
42
42
  raise ValueError(f"Invalid anchor id {anchor_id} !")
43
43
  return default
44
44
 
45
- def validate_access(self) -> bool:
46
- """Validate access."""
47
- return self.root.has_read_access(self.entry_node)
48
-
49
45
  def set_entry_node(self, entry_node: str | None) -> None:
50
46
  """Override entry."""
51
47
  self.entry_node = self.init_anchor(entry_node, self.root)
@@ -4,6 +4,7 @@ import inspect
4
4
  import marshal
5
5
  import os
6
6
  import sys
7
+ import tempfile
7
8
  import types
8
9
  from contextvars import ContextVar
9
10
  from typing import Optional, Union
@@ -111,6 +112,62 @@ class JacMachine:
111
112
  return nodes
112
113
  return []
113
114
 
115
+ def create_architype_from_source(
116
+ self,
117
+ source_code: str,
118
+ module_name: Optional[str] = None,
119
+ base_path: Optional[str] = None,
120
+ cachable: bool = False,
121
+ keep_temporary_files: bool = False,
122
+ ) -> Optional[types.ModuleType]:
123
+ """Dynamically creates architypes (nodes, walkers, etc.) from Jac source code."""
124
+ from jaclang.runtimelib.importer import JacImporter, ImportPathSpec
125
+
126
+ if not base_path:
127
+ base_path = self.base_path or os.getcwd()
128
+
129
+ if base_path and not os.path.exists(base_path):
130
+ os.makedirs(base_path)
131
+ if not module_name:
132
+ module_name = f"_dynamic_module_{len(self.loaded_modules)}"
133
+ with tempfile.NamedTemporaryFile(
134
+ mode="w",
135
+ suffix=".jac",
136
+ prefix=module_name + "_",
137
+ dir=base_path,
138
+ delete=False,
139
+ ) as tmp_file:
140
+ tmp_file_path = tmp_file.name
141
+ tmp_file.write(source_code)
142
+
143
+ try:
144
+ importer = JacImporter(self)
145
+ tmp_file_basename = os.path.basename(tmp_file_path)
146
+ tmp_module_name, _ = os.path.splitext(tmp_file_basename)
147
+
148
+ spec = ImportPathSpec(
149
+ target=tmp_module_name,
150
+ base_path=base_path,
151
+ absorb=False,
152
+ cachable=cachable,
153
+ mdl_alias=None,
154
+ override_name=module_name,
155
+ lng="jac",
156
+ items=None,
157
+ )
158
+
159
+ import_result = importer.run_import(spec, reload=False)
160
+ module = import_result.ret_mod
161
+
162
+ self.loaded_modules[module_name] = module
163
+ return module
164
+ except Exception as e:
165
+ logger.error(f"Error importing dynamic module '{module_name}': {e}")
166
+ return None
167
+ finally:
168
+ if not keep_temporary_files and os.path.exists(tmp_file_path):
169
+ os.remove(tmp_file_path)
170
+
114
171
  def update_walker(
115
172
  self, module_name: str, items: Optional[dict[str, Union[str, Optional[str]]]]
116
173
  ) -> tuple[types.ModuleType, ...]:
@@ -82,8 +82,6 @@ class ShelfStorage(Memory[UUID, Anchor]):
82
82
  if isinstance(self.__shelf__, Shelf):
83
83
  from jaclang.plugin.feature import JacFeature as Jac
84
84
 
85
- root = Jac.get_root().__jac__
86
-
87
85
  for anchor in self.__gc__:
88
86
  self.__shelf__.pop(str(anchor.id), None)
89
87
  self.__mem__.pop(anchor.id, None)
@@ -96,14 +94,14 @@ class ShelfStorage(Memory[UUID, Anchor]):
96
94
  isinstance(p_d, NodeAnchor)
97
95
  and isinstance(d, NodeAnchor)
98
96
  and p_d.edges != d.edges
99
- and root.has_connect_access(d)
97
+ and Jac.check_connect_access(d)
100
98
  ):
101
99
  if not d.edges:
102
100
  self.__shelf__.pop(_id, None)
103
101
  continue
104
102
  p_d.edges = d.edges
105
103
 
106
- if root.has_write_access(d):
104
+ if Jac.check_write_access(d):
107
105
  if hash(dumps(p_d.access)) != hash(dumps(d.access)):
108
106
  p_d.access = d.access
109
107
  if hash(dumps(p_d.architype)) != hash(dumps(d.architype)):
@@ -0,0 +1,7 @@
1
+ node UtilityNode {
2
+ has data: int;
3
+
4
+ can display_data with entry {
5
+ print("UtilityNode Data:", f'{self.data}');
6
+ }
7
+ }
@@ -0,0 +1,30 @@
1
+ import:py from jaclang.runtimelib.machine { JacMachine }
2
+
3
+ glob dynamic_module_source = """
4
+ import from .arch_create_util {UtilityNode}
5
+
6
+ walker DynamicWalker {
7
+ can start with entry {
8
+ print("DynamicWalker Started");
9
+ here ++> UtilityNode(data=42);
10
+ visit [-->](`?UtilityNode);
11
+ }
12
+
13
+ can UtilityNode {
14
+ here.display_data();
15
+ }
16
+ }
17
+ """;
18
+
19
+ with entry {
20
+ node_arch = JacMachine.get().create_architype_from_source(
21
+ dynamic_module_source,
22
+ module_name="dynamic_module"
23
+ );
24
+ walker_obj = JacMachine.get().spawn_walker(
25
+ 'DynamicWalker',
26
+ module_name="dynamic_module",
27
+
28
+ );
29
+ root spawn walker_obj;
30
+ }
@@ -0,0 +1,35 @@
1
+ import:py from jaclang.runtimelib.machine { JacMachine }
2
+ # Dynamically create a node architype
3
+ glob source_code = """
4
+ node dynamic_node {
5
+ has value:int;
6
+ can print_value with entry {
7
+ print("Dynamic Node Value:", f'{self.value}');
8
+ }
9
+ }
10
+ """;
11
+
12
+ # Create a new walker architype dynamically
13
+ glob walker_code = """
14
+ walker dynamic_walker {
15
+ can visit_nodes with entry {
16
+ visit [-->](`?dynamic_node);
17
+ }
18
+ }
19
+ """;
20
+
21
+ with entry {
22
+ node_arch = JacMachine.get().create_architype_from_source(source_code);
23
+ walker_arch = JacMachine.get().create_architype_from_source(walker_code);
24
+
25
+ node_obj = JacMachine.get().spawn_node(
26
+ 'dynamic_node',
27
+ {'value': 99},
28
+ node_arch.__name__
29
+ );
30
+ walker_obj = JacMachine.get().spawn_walker(
31
+ 'dynamic_walker',
32
+ module_name=walker_arch.__name__
33
+ );
34
+ node_obj spawn walker_obj;
35
+ }
@@ -37,7 +37,7 @@ edge Edge_c {
37
37
 
38
38
  with entry {
39
39
  print(root spawn creator());
40
- print(root.__jac__.gen_dot());
40
+ print(Jac.node_dot(root));
41
41
  print([root-:Edge_a:->]);
42
42
  print([root-:Edge_c:->]);
43
43
  print([root-:Edge_a:->-:Edge_b:->]);
@@ -30,7 +30,7 @@ edge Edge_c{
30
30
 
31
31
  with entry{
32
32
  print(root spawn creator());
33
- print(root.__jac__.gen_dot());
33
+ print(Jac.node_dot(root));
34
34
  print([root -:Edge_a:->]);
35
35
  print([root -:Edge_c:->]);
36
36
  print([root -:Edge_a:-> -:Edge_b:->]);
@@ -73,5 +73,5 @@ with entry {
73
73
  root spawn walker1();
74
74
  root spawn walker2();
75
75
  root spawn walker3();
76
- print(root.__jac__.gen_dot());
76
+ print(Jac.node_dot(root));
77
77
  }
@@ -0,0 +1,20 @@
1
+ node MyNode{
2
+ has Name:str;
3
+ }
4
+
5
+ edge a{}
6
+
7
+ edge b{}
8
+
9
+ with entry{
10
+ Start = MyNode("Start");
11
+ End = MyNode("End");
12
+ mid = MyNode("Middle");
13
+ root <+:a:+ Start;
14
+ root +:a:+> End;
15
+ root +:b:+> mid;
16
+ root +:a:+> mid;
17
+
18
+ print([root-->]);
19
+
20
+ }