llparse 0.1.0__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.
llparse/settings.py ADDED
@@ -0,0 +1,285 @@
1
+ """
2
+ Compiles a settings module to use alongside llparse
3
+
4
+ This is mostly a concept idea for llparse inspired by llhttp to include a settings
5
+ module in the future to make Writing the span callbacks and other important things
6
+ less painful to edit and write entirely.
7
+
8
+ NOTE: That The objects being passed would come directly from llparse-builder
9
+
10
+ """
11
+
12
+ from dataclasses import dataclass, field
13
+ from typing import Optional, Union
14
+
15
+ from .llparse import LLParse
16
+ from .pybuilder import main_code as source
17
+
18
+ # MACRO Names , These can be renamed to
19
+ # prevent mixing with llhttp or other parsers
20
+ # allow for mulitple different parsers to be
21
+ # used at the same time...
22
+
23
+ CALLBACK_MAYBE = "CALLBACK_MAYBE"
24
+ SPAN_CALLBACK_MAYBE = "SPAN_CALLBACK_MAYBE"
25
+ MID_POSTFIX = "_settings"
26
+
27
+
28
+
29
+ @dataclass
30
+ class ApiResult:
31
+ header: str
32
+ c: str
33
+
34
+
35
+ @dataclass
36
+ class Settings:
37
+ name: str
38
+ """The property name to take from the settings module..."""
39
+ prefix: str
40
+ """The Prefix of your parser"""
41
+ postfix: Optional[str] = None
42
+ """The postfix of your parser"""
43
+ callbacks: dict[str, bool] = field(default_factory=dict)
44
+ """These are callbacks to be later edited out into thier appropreate places"""
45
+ files: dict[str, dict[str, str]] = field(default_factory=dict)
46
+ """Used to sort out functions if they are to be moved to different places these
47
+ files will not affect the main api and can be ignored if chosen to be..."""
48
+
49
+ external_execute_func: str = field(default_factory=str, init=False)
50
+ """Allows the use of an external function and it's prefix to be called
51
+ ```c
52
+
53
+ int POSTFIX_execute(PREFIX_t* parser, const char* data, size_t len) {
54
+ return PREFIX_execute(parser, data, data + len);
55
+ }
56
+
57
+ ```
58
+ WARNIG! THIS SHOULD ONLY BE USED IF A POSTFIX HAS BEEN ADDED!"""
59
+
60
+ # It can be annoying to have to recall something over and over again so I added the ability to use
61
+ # Nodes and Code.Match classes to make up for this annoyance...
62
+ def move_cb(
63
+ self,
64
+ callback: Union[str, source._Match, source.Node, source.Invoke],
65
+ new_location: str,
66
+ code: Optional[str] = None,
67
+ ):
68
+ """Moves the callback chosen to another file instead of the name of your api
69
+ this can help you add things like custom C code, to non-api or user-interacted parts...
70
+ """
71
+
72
+ # Grab real name if the callback is a Node or Match otherwise get the string for it...
73
+
74
+ name = (
75
+ callback.name
76
+ if isinstance(callback, (source.Code, source.Node))
77
+ else callback
78
+ )
79
+ # If not added make a new dictioray / Map if your on typescript...
80
+ if not self.files.get(new_location):
81
+ self.files[new_location] = {}
82
+
83
+ self.files[new_location][name] = code
84
+
85
+ # Remove from default callbacks to be moved to another file in C...
86
+ # If index fails (IndexError) the callback name doesn't exist...
87
+ self.callbacks.pop(name)
88
+ return self
89
+
90
+ def add_external_execute(self, postfix_as_struct: bool = False):
91
+ """Adds an external execute function as long as a postfix variable exists otherwise
92
+ using this function this is not nessesary!
93
+ ```c
94
+ int POSTFIX_execute(PREFIX_t* parser, const char* data, size_t len) {
95
+ return PREFIX_execute(parser, data, data + len);
96
+ }
97
+ ```
98
+ Parameters
99
+ ---
100
+ `postfix_as_struct` use postfix struct instead of inside of POSTFIX_execute's function...
101
+ """
102
+ if self.postfix:
103
+ self.external_execute_func += (
104
+ f"int {self.postfix}_execute({self.postfix if postfix_as_struct else self.prefix}_t* parser, const char* data, size_t len) "
105
+ + "{"
106
+ )
107
+ self.external_execute_func += (
108
+ f"\nreturn {self.prefix}_execute(parser, data, data + len);\n" + "}"
109
+ )
110
+
111
+ return self
112
+
113
+ def build_functions(self, out: list[str]):
114
+ # Added newline to this one to reamin consistant with llhttp if the library devs decided to
115
+ # implement my idea into thier projects
116
+ out.append("/* CALLBACKS */\n")
117
+ out.append("")
118
+ # TODO Vizonex allow custom enum names to be passed as well in the next concept...
119
+ for cb, is_span in self.callbacks.items():
120
+ out.append(
121
+ f"int {cb}({self.prefix}_t * s, const char* p, const char* endp) " + "{"
122
+ )
123
+ out.append(" int err;")
124
+ if is_span:
125
+ out.append(f" {SPAN_CALLBACK_MAYBE}(s, {cb} ,p, endp - p);")
126
+ else:
127
+ out.append(f" {CALLBACK_MAYBE}(s, {cb});")
128
+ out.append(" return err;")
129
+ # Close function and give some room for the next one to be placed into...
130
+ out.append("}")
131
+ out.append("")
132
+ return
133
+
134
+ def buildMacros(
135
+ self,
136
+ out: list[str],
137
+ callback: str = CALLBACK_MAYBE,
138
+ span_callback: str = SPAN_CALLBACK_MAYBE,
139
+ ):
140
+ """Builds Global Macro Callbacks"""
141
+ # Inspired by llhttp
142
+ out.append(
143
+ f"#define {callback}(PARSER, NAME) \\"
144
+ )
145
+ out.append(
146
+ "do { \\"
147
+ )
148
+ out.append(
149
+ f" const {self.prefix}{self.name}_t* settings; \\"
150
+ )
151
+ out.append(
152
+ f" settings = (const {self.prefix}{self.name}_t*) (PARSER)->{self.name}; \\"
153
+ )
154
+ out.append(
155
+ " if (settings == NULL || settings->NAME == NULL) { \\"
156
+ )
157
+ out.append(
158
+ " err = 0; \\"
159
+ )
160
+ out.append(
161
+ " break; \\"
162
+ )
163
+ out.append(
164
+ " } \\"
165
+ )
166
+ out.append(
167
+ " err = settings->NAME((PARSER)); \\"
168
+ )
169
+ out.append("} while (0)")
170
+ out.append("")
171
+
172
+ out.append(
173
+ f"#define {span_callback}(PARSER, NAME, START, LEN) \\"
174
+ )
175
+ out.append(
176
+ " do { \\"
177
+ )
178
+ out.append(
179
+ f" const {self.prefix}{self.name}_t* settings; \\"
180
+ )
181
+ out.append(
182
+ f" settings = (const {self.prefix}{self.name}_t*) (PARSER)->{self.name}; \\"
183
+ )
184
+ out.append(
185
+ " if (settings == NULL || settings->NAME == NULL) { \\"
186
+ )
187
+ out.append(
188
+ " err = 0; \\"
189
+ )
190
+ out.append(
191
+ " break; \\"
192
+ )
193
+ out.append(
194
+ " } \\"
195
+ )
196
+ out.append(
197
+ " err = settings->NAME((PARSER), (START), (LEN)); \\"
198
+ )
199
+ # TODO: maybe allow for custom error functions to be raised right here
200
+ # or from or with both of them optionally? --
201
+ out.append(" } while (0)")
202
+ out.append("\n\n\n")
203
+
204
+ def build_C(self, cb: str, span_cb: str):
205
+ out = []
206
+ out.append("#include <stdlib.h>")
207
+ out.append("#include <stdio.h>")
208
+ out.append("#include <string.h>")
209
+ out.append("")
210
+ out.append(f'#include "{self.prefix}.h"')
211
+ out.append("")
212
+ self.buildMacros(out, cb, span_cb)
213
+ self.build_functions(out)
214
+ return "\n".join(out)
215
+
216
+ def build_H(self):
217
+ out = []
218
+
219
+ fix = self.postfix if self.postfix else self.prefix
220
+
221
+ out.append(f"typedef struct {fix}{self.name}_s {fix}{self.name}_t;")
222
+ out.append(
223
+ f"typedef int (*{fix}_data_cb)({fix}_t*, const char *at, size_t length);"
224
+ )
225
+ out.append(f"typedef int (*{fix}_cb)({fix}_t*);")
226
+
227
+ out.append(f"struct {self.prefix}{self.name}_s " + "{")
228
+ for cb, is_span in self.callbacks.items():
229
+ if is_span:
230
+ out.append(f" {fix}_data_cb {cb};")
231
+ else:
232
+ out.append(f" {fix}_cb {cb};")
233
+ out.append("};")
234
+
235
+
236
+ class Disassembler:
237
+ """Takes apart LLParse's calls and sorts the data out to help make
238
+ different things such as a settings api moudle, Cython pxd file
239
+ and a Typescript Writer, and a Json model of what's going on...
240
+ """
241
+
242
+ def __init__(self, builder: LLParse) -> None:
243
+ self.invokes: set[source.Invoke] = set()
244
+ self.nodes: dict[str, source.Node] = {}
245
+ """Used to create a small dictionary of all avalible nodes to thier corresponding names.
246
+ this also includes it's edges used and seen to create a json file if chosen."""
247
+ self.empty_apis: dict[str, bool] = {}
248
+ """Carries some of the main api calls to make in a settings module...
249
+ These carry thier own boolean to see weather or not the callback is a `Span` (True) ,
250
+ `Match` (False)..."""
251
+ self.prefix = builder.prefix
252
+ self.properties = builder.privProperties
253
+ """Carries important property values"""
254
+
255
+ def disassemble(self, root: source.Node):
256
+ """Takes root apart and enumerates through all the major parts
257
+ returns itself as a convenience measure..."""
258
+ reach = source.Reachability()
259
+ roots = reach.build(root)
260
+ # Get all span's names and objects themselves...
261
+ # Turn span_names into a set
262
+ self.nodes = {n.name: n for n in roots}
263
+ # print(self.nodes)
264
+ span_names = list(
265
+ sorted(
266
+ {
267
+ n.span.callback.name
268
+ for n in roots
269
+ if isinstance(n, (source.SpanStart, source.SpanEnd))
270
+ }
271
+ )
272
+ )
273
+ # span_starts = {n.span.callback.name:n for n in roots if isinstance(n,source.SpanStart)}
274
+
275
+ self.empty_apis.update({sn: True for sn in span_names})
276
+
277
+ invokes = [n for n in roots if isinstance(n, source.Invoke)]
278
+
279
+ for i in invokes:
280
+ # I have Match Underscored because in my version of the library there's two of them in the same file.
281
+ # The one with the underscore is for use with Invoke...
282
+ if isinstance(i.code, source._Match):
283
+ self.empty_apis[i.code.name] = False
284
+
285
+ return self
llparse/spanalloc.py ADDED
@@ -0,0 +1,176 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Union
3
+
4
+ from .pybuilder.main_code import Node, Reachability, Span, SpanEnd, SpanStart
5
+
6
+
7
+ class DeadLoop(Exception):
8
+ """Thrown when this type of loop is detected during complation
9
+ ```c
10
+ switch(*p){
11
+ case dead_loop:
12
+ dead_loop : {
13
+ goto dead_loop;
14
+ } /* Rest in Peace Computer x_x */
15
+ }
16
+
17
+ ```"""
18
+
19
+
20
+ SpanSet = set[Span]
21
+
22
+
23
+ def _id(node: Union[SpanStart, SpanEnd]):
24
+ return node.span
25
+
26
+
27
+ @dataclass
28
+ class ISpanActiveInfo:
29
+ active: dict[Node, SpanSet] = field(default_factory=dict)
30
+ spans: list[Span] = field(default_factory=list)
31
+
32
+
33
+ SpanOverlap = dict[Node, SpanSet]
34
+
35
+
36
+ @dataclass
37
+ class ISpanAllocatorResult:
38
+ colors: dict[Span, int] = field(default_factory=dict)
39
+ concurrency: list[list[Span]] = field(default_factory=list)
40
+ max: int = field(default_factory=int)
41
+
42
+
43
+ class SpanAllocator:
44
+ def __init__(self) -> None:
45
+ return
46
+
47
+ def allocate(self, root: Node):
48
+ nodes = Reachability.build(root)
49
+ info = self.computeActive(nodes)
50
+
51
+ self.check(info)
52
+ overlap = self.computeOverlap(info)
53
+ return self.color(info.spans, overlap)
54
+
55
+ def check(self, info: ISpanActiveInfo):
56
+ for node, spans in info.active.items():
57
+ for edge in node.getAllEdges():
58
+ if isinstance(edge.node, SpanStart):
59
+ continue
60
+
61
+ # Skip terminal nodes
62
+ # print(len(edge.node.getAllEdges()))
63
+ # print(info.active)
64
+ if len(edge.node.getAllEdges()) == 0:
65
+ continue
66
+
67
+ # assert node.name != edge.node.name
68
+ # print("checking edge from %s to %s" % (node.name,edge.node.name))
69
+ # check edge
70
+
71
+ edgeSpans: set[Span] = info.active[edge.node]
72
+ # print("SPAN:%s NODE:%s" % (edgeSpans,edge.node.__dict__))
73
+ for subSpan in edgeSpans:
74
+ if subSpan not in spans:
75
+ raise AssertionError(
76
+ f'Unmatched span end for "{subSpan.callback.name}"'
77
+ / f'at "{edge.node.name}", coming from "{node.name}"'
78
+ )
79
+
80
+ if isinstance(edge.node, SpanEnd):
81
+ span = _id(edge.node)
82
+ if span not in spans:
83
+ raise AssertionError(
84
+ f'Unmatched span end for "{span.callback.name}"'
85
+ )
86
+
87
+ def computeActive(self, nodes: list[Node]):
88
+ activeMap: dict[Node, SpanSet] = dict()
89
+ for node in nodes:
90
+ activeMap[node] = set()
91
+
92
+ queue = set(nodes)
93
+ spans: SpanSet = set()
94
+ # This fixes an issue when using a for loop which unlike in typescript
95
+ # we cannot remove items when in a for-loop in python this also ensures
96
+ # that all spans are visited.
97
+ while queue:
98
+ node = queue.pop()
99
+ active = activeMap[node]
100
+ if isinstance(node, SpanStart):
101
+ span = _id(node)
102
+ spans.add(span)
103
+ active.add(span)
104
+
105
+ for span in active:
106
+ if isinstance(node, SpanEnd) and span == _id(node):
107
+ break
108
+
109
+ for edge in node.getAllEdges():
110
+ edgeNode = edge.node
111
+
112
+ if isinstance(edgeNode, SpanStart):
113
+ if _id(edgeNode) == span:
114
+ raise DeadLoop(
115
+ f'Detected loop in span {span.callback.name} at "{node.name}"'
116
+ )
117
+
118
+ edgeActive = activeMap[edgeNode]
119
+ if span in edgeActive:
120
+ break
121
+
122
+ edgeActive.add(span)
123
+ queue.add(edgeNode)
124
+
125
+ return ISpanActiveInfo(active=activeMap, spans=list(spans))
126
+
127
+ def computeOverlap(self, info: ISpanActiveInfo):
128
+ active = info.active
129
+
130
+ overlap: dict[Span, set[Span]] = {span: set() for span in info.spans}
131
+ for _, spans in active.items():
132
+ for one in spans:
133
+ for other in spans:
134
+ if other != one:
135
+ overlap[one].add(other)
136
+ return overlap
137
+
138
+ def _allocate(self, span: Span):
139
+ if self._colors.get(span):
140
+ return self._colors[span]
141
+
142
+ overlap = self._overlapMap[span]
143
+
144
+ used: set[int] = set()
145
+ for subSpan in overlap:
146
+ if self._colors.get(subSpan):
147
+ used.add(self._colors.get(subSpan))
148
+ i = 0
149
+ while i in used:
150
+ i += 1
151
+
152
+ self._mx = max(self._mx, i)
153
+ self._colors[span] = i
154
+ return i
155
+
156
+ def color(self, spans: list[Span], overlapDict: SpanOverlap):
157
+ # Used _max instead of max because max() is an api called function needed in a bit...
158
+ self._mx = -1
159
+ self._colors: dict[Span, int] = {}
160
+
161
+ self._overlapMap = overlapDict
162
+
163
+ Map = {span: self._allocate(span) for span in spans}
164
+
165
+ concurrency = list()
166
+ for _ in range(self._mx + 1):
167
+ # NOTE : concurrency[i] = [] doesn't work but this does :P
168
+ concurrency.append([])
169
+
170
+ for s in spans:
171
+ concurrency[self._allocate(s)].append(s)
172
+
173
+ return ISpanAllocatorResult(Map, concurrency, self._mx)
174
+
175
+
176
+ # TODO (Vizonex) Use Indutny's Mini Http parser to help with testing ours to verify that ours is correct...
llparse/test.py ADDED
@@ -0,0 +1,232 @@
1
+ from .C_compiler import Compilation, ICompilerOptions
2
+ from .frontend import Frontend
3
+ from .llparse import LLParse
4
+ from .pybuilder import Builder
5
+
6
+ # TODO: Remove and make pytest for it.
7
+
8
+ def smaller_test():
9
+ # from compilator import Compilation, ICompilerOptions
10
+ p = Builder()
11
+ simple = p.node("simple")
12
+ complete = p.invoke(
13
+ p.code.match("On_Complete"), {0: simple}, p.error(1, "On_Complete FAILED!")
14
+ )
15
+
16
+ simple.peek("\n", complete).skipTo(simple)
17
+
18
+ front = Frontend("http_parser")
19
+ f = front.compile(simple, properties=p.properties)
20
+ comp = Compilation(
21
+ "http_parser", f.properties, f.resumptionTargets, ICompilerOptions(None, None)
22
+ )
23
+ # for r in comp.resumptionTargets:
24
+ # print(r)
25
+ out = []
26
+ root = comp.unwrapNode(f.root)
27
+ root.build(comp)
28
+ comp.reserveSpans(f.spans)
29
+ comp.buildGlobals(out)
30
+ comp.buildResumptionStates(out)
31
+ print(comp.stateDict)
32
+ # print("\n".join(out))
33
+
34
+
35
+ def test1():
36
+ # I will be using indutny's mini http parser to demonstrate as it's
37
+ # medium sized and uses all major tricks , bells and whistles...
38
+
39
+ p = Builder()
40
+ method = p.node("method")
41
+ beforeUrl = p.node("before_url")
42
+ urlSpan = p.span(p.code.span("on_url"))
43
+ url = p.node("url")
44
+ http = p.node("http")
45
+
46
+ # print(sliced)(method)
47
+
48
+ # // Add custom uint8_t property to the state
49
+ p.property("i8", "method")
50
+
51
+ # Store method inside a custom property
52
+ onMethod = p.invoke(p.code.store("method"), beforeUrl)
53
+
54
+ # Invoke custom C function
55
+ complete = p.invoke(
56
+ p.code.match("on_complete"),
57
+ {
58
+ # Restart
59
+ 0: method
60
+ },
61
+ p.error(4, "`on_complete` error"),
62
+ )
63
+
64
+ method.select(
65
+ {
66
+ "HEAD": 0,
67
+ "GET": 1,
68
+ "POST": 2,
69
+ "PUT": 3,
70
+ "DELETE": 4,
71
+ "OPTIONS": 5,
72
+ "CONNECT": 6,
73
+ "TRACE": 7,
74
+ "PATCH": 8,
75
+ },
76
+ onMethod,
77
+ ).otherwise(p.error(5, "Expected method"))
78
+
79
+ beforeUrl.match(" ", beforeUrl).otherwise(urlSpan.start(url))
80
+
81
+ url.peek(" ", urlSpan.end(http)).skipTo(url)
82
+
83
+ http.match(" HTTP/1.1\r\n\r\n", complete).otherwise(
84
+ p.error(6, "Expected HTTP/1.1 and two newlines")
85
+ )
86
+
87
+ front = Frontend("http_parser")
88
+ f = front.compile(
89
+ method,
90
+ properties=p.properties,
91
+ )
92
+ # r = f.root.ref.id.name
93
+ # NOTE Check that root was addded , this can be a problem laster down the line if it isn't a resumption target...
94
+ assert f.root in f.resumptionTargets
95
+ comp = Compilation(
96
+ "http_parser", p.properties(), f.resumptionTargets, ICompilerOptions()
97
+ )
98
+ root = comp.unwrapNode(f.root)
99
+ root.build(comp)
100
+
101
+ assert "s_n_http_parser__n_method" in comp.resumptionTargets
102
+ out = []
103
+ comp.reserveSpans(f.spans)
104
+ comp.buildGlobals(out)
105
+ out.append("")
106
+ out.append("/*--RESUMPTION STATES--*/")
107
+ comp.buildResumptionStates(out)
108
+ out.append("/*--INTERNAL STATES--*/")
109
+ comp.buildInternalStates(out)
110
+ # for k , v in comp.stateDict.items():
111
+ # print(k)
112
+ print("\n".join(out))
113
+
114
+
115
+ def test2():
116
+ from llparse import LLParse
117
+
118
+ p = LLParse("http_parser")
119
+ method = p.node("method")
120
+ beforeUrl = p.node("before_url")
121
+ urlSpan = p.span(p.code.span("on_url"))
122
+ url = p.node("url")
123
+ http = p.node("http")
124
+
125
+ # Add custom uint8_t property to the state
126
+ p.property("i8", "method")
127
+
128
+ # Store method inside a custom property
129
+ onMethod = p.invoke(p.code.store("method"), beforeUrl)
130
+
131
+ # Invoke custom C function
132
+ complete = p.invoke(
133
+ p.code.match("on_complete"),
134
+ {
135
+ # Restart
136
+ 0: method
137
+ },
138
+ p.error(4, "`on_complete` error"),
139
+ )
140
+
141
+ method.select(
142
+ {
143
+ "HEAD": 0,
144
+ "GET": 1,
145
+ "POST": 2,
146
+ "PUT": 3,
147
+ "DELETE": 4,
148
+ "OPTIONS": 5,
149
+ "CONNECT": 6,
150
+ "TRACE": 7,
151
+ "PATCH": 8,
152
+ },
153
+ onMethod,
154
+ ).otherwise(p.error(5, "Expected method"))
155
+
156
+ beforeUrl.match(" ", beforeUrl).otherwise(urlSpan.start(url))
157
+
158
+ url.peek(" ", urlSpan.end(http)).skipTo(url)
159
+
160
+ http.match(" HTTP/1.1\r\n\r\n", complete).otherwise(
161
+ p.error(6, "Expected HTTP/1.1 and two newlines")
162
+ )
163
+
164
+ c = p.build(method)
165
+ print(c.c)
166
+ open("http_parser.c", "w").write(c.c)
167
+ open("http_parser.h", "w").write(c.header)
168
+ # # sp_alloc = SpanAllocator().allocate(method)
169
+
170
+ # TODO Validate Moving parts as they should be compiled correctly...
171
+
172
+
173
+ def test3():
174
+ """Moving Part Validations..."""
175
+ # A~Sample~Of~Dual Spans...
176
+ p = LLParse("dual_spans")
177
+
178
+ span_a = p.span(p.code.span("on_a"))
179
+ span_b = p.span(p.code.span("on_b"))
180
+
181
+ a = p.node("a_node")
182
+ b = p.node("b_node")
183
+
184
+ start = p.node("start")
185
+ start.otherwise(span_a.start(a))
186
+
187
+ a.peek("~", span_a.end().skipTo(span_b.start(b))).skipTo(a)
188
+ b.peek("~", span_b.end().skipTo(span_a.start(a))).skipTo(b)
189
+
190
+ compiled = p.build(start)
191
+ print(compiled.c)
192
+
193
+
194
+ def test4():
195
+ """Same Dual Spans but with flags..."""
196
+ # A~Sample~Of~Dual Spans...
197
+
198
+ p = LLParse("dual_spans")
199
+
200
+ # Flag
201
+ p.property("i8", "flag")
202
+
203
+ span_a = p.span(p.code.span("on_a"))
204
+ span_b = p.span(p.code.span("on_b"))
205
+
206
+ a = p.node("a_node")
207
+ b = p.node("b_node")
208
+
209
+ # Invoke Updates to Flag
210
+ # 1 is a , 2 is b
211
+
212
+ update_a = p.invoke(p.code.update("flag", 1), span_a.start(a))
213
+ update_b = p.invoke(p.code.update("flag", 2), span_b.start(b))
214
+ start = p.node("start")
215
+ start.otherwise(update_a)
216
+
217
+ a.peek("~", span_a.end().skipTo(update_a)).skipTo(a)
218
+ b.peek("~", span_b.end().skipTo(update_b)).skipTo(b)
219
+
220
+ compiled = p.build(start)
221
+ print(compiled.c)
222
+
223
+
224
+ # TODO Try to Throw more complicated and advanced things at pyllparse to try and solve
225
+ # This will make the code sturdier and more mature as time goes on
226
+ # Make a Test Bag on github for all of these tests to go into incase a single file gets too big
227
+
228
+ # I encourage anyone to find valid compiled llparse sequences and then translate them over to
229
+ # python to testrun , I will try to make a validator tool whenever I can get to it - Vizonex
230
+
231
+ if __name__ == "__main__":
232
+ test2()