@panagos/jojoscript 0.7.0 → 0.8.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/README.md +429 -418
- package/bin/jojo.js +430 -170
- package/package.json +50 -59
- package/src/compiler.js +197 -169
- package/src/pipeline-hash.js +27 -0
- package/src/pipeline-ir.js +100 -0
- package/src/pipeline-parser.js +583 -418
- package/src/runtime/checkpoint-store.js +122 -0
- package/src/runtime/checkpoint.js +69 -0
- package/src/runtime/collections.js +438 -327
- package/src/runtime/errors.js +29 -0
- package/src/runtime/execution-context.js +86 -0
- package/src/runtime/index.js +51 -29
- package/src/runtime/inspect.js +62 -0
- package/src/runtime/metrics.js +41 -0
- package/src/runtime/retry.js +67 -0
- package/src/runtime/trace.js +66 -0
package/src/pipeline-parser.js
CHANGED
|
@@ -1,418 +1,583 @@
|
|
|
1
|
-
import {
|
|
2
|
-
tokenize,
|
|
3
|
-
significantTokens,
|
|
4
|
-
maskNonCode
|
|
5
|
-
} from './lexer.js'
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
'
|
|
11
|
-
'
|
|
12
|
-
'
|
|
13
|
-
'
|
|
14
|
-
'
|
|
15
|
-
'
|
|
16
|
-
'
|
|
17
|
-
'
|
|
18
|
-
'
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
'
|
|
22
|
-
'
|
|
23
|
-
'
|
|
24
|
-
'
|
|
25
|
-
'
|
|
26
|
-
'
|
|
27
|
-
'
|
|
28
|
-
'
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
'
|
|
33
|
-
'
|
|
34
|
-
'
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
if (
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
if (
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
if (
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
|
|
1
|
+
import {
|
|
2
|
+
tokenize,
|
|
3
|
+
significantTokens,
|
|
4
|
+
maskNonCode
|
|
5
|
+
} from './lexer.js'
|
|
6
|
+
|
|
7
|
+
import { computePipelineId } from './pipeline-hash.js'
|
|
8
|
+
|
|
9
|
+
const PIPELINE_STDLIB = new Set([
|
|
10
|
+
'map',
|
|
11
|
+
'filter',
|
|
12
|
+
'sort',
|
|
13
|
+
'unique',
|
|
14
|
+
'take',
|
|
15
|
+
'skip',
|
|
16
|
+
'flatMap',
|
|
17
|
+
'reduce',
|
|
18
|
+
'toArray',
|
|
19
|
+
'count',
|
|
20
|
+
'first',
|
|
21
|
+
'find',
|
|
22
|
+
'some',
|
|
23
|
+
'every',
|
|
24
|
+
'groupBy',
|
|
25
|
+
'partition',
|
|
26
|
+
'chunk',
|
|
27
|
+
'window',
|
|
28
|
+
'zip',
|
|
29
|
+
'zipWith',
|
|
30
|
+
'scan',
|
|
31
|
+
'sortBy',
|
|
32
|
+
'distinctBy',
|
|
33
|
+
'tap',
|
|
34
|
+
'mapAsync',
|
|
35
|
+
'filterAsync',
|
|
36
|
+
'toArrayAsync',
|
|
37
|
+
'checkpoint',
|
|
38
|
+
'inspect',
|
|
39
|
+
'parallel',
|
|
40
|
+
'batch',
|
|
41
|
+
'retry'
|
|
42
|
+
])
|
|
43
|
+
|
|
44
|
+
/*
|
|
45
|
+
* These stages are "runtime-aware": the compiler auto-appends a hidden,
|
|
46
|
+
* literal metadata argument (pipeline id, stable node id, stage type) to
|
|
47
|
+
* their call so the runtime operator can identify itself for
|
|
48
|
+
* checkpointing/profiling/error-reporting without the user ever writing
|
|
49
|
+
* that plumbing by hand.
|
|
50
|
+
*/
|
|
51
|
+
const RUNTIME_AWARE_STAGES = new Set(['checkpoint', 'inspect', 'parallel', 'batch'])
|
|
52
|
+
|
|
53
|
+
/*
|
|
54
|
+
* A `retry(...)` stage immediately following one of these fuses into that
|
|
55
|
+
* stage's function argument (per-item retry) instead of becoming its own
|
|
56
|
+
* pipeline stage. See `fuseRetryStages`.
|
|
57
|
+
*/
|
|
58
|
+
const MAP_LIKE_STAGES = new Set(['map', 'mapAsync', 'filterAsync', 'tap'])
|
|
59
|
+
|
|
60
|
+
/*
|
|
61
|
+
* These stage names are handled at compile time instead of being resolved
|
|
62
|
+
* to a runtime function call, so they must never be added as automatic
|
|
63
|
+
* `jojoscript/runtime` imports.
|
|
64
|
+
*/
|
|
65
|
+
const PIPELINE_CONTROL_STAGES = new Set(['await', 'catch', 'orElse'])
|
|
66
|
+
|
|
67
|
+
function text(source, tokens) {
|
|
68
|
+
if (!tokens.length) return ''
|
|
69
|
+
return source.slice(tokens[0].start, tokens[tokens.length - 1].end)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function matchingToken(tokens, start, open, close) {
|
|
73
|
+
let depth = 0
|
|
74
|
+
for (let i = start; i < tokens.length; i++) {
|
|
75
|
+
if (tokens[i].value === open) depth++
|
|
76
|
+
if (tokens[i].value === close) {
|
|
77
|
+
depth--
|
|
78
|
+
if (depth === 0) return i
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
throw new SyntaxError(`Unclosed ${open}`)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function splitArguments(tokens) {
|
|
85
|
+
const result = []
|
|
86
|
+
let start = 0
|
|
87
|
+
let parens = 0
|
|
88
|
+
let braces = 0
|
|
89
|
+
let brackets = 0
|
|
90
|
+
|
|
91
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
92
|
+
const value = tokens[i].value
|
|
93
|
+
if (value === '(') parens++
|
|
94
|
+
else if (value === ')') parens--
|
|
95
|
+
else if (value === '{') braces++
|
|
96
|
+
else if (value === '}') braces--
|
|
97
|
+
else if (value === '[') brackets++
|
|
98
|
+
else if (value === ']') brackets--
|
|
99
|
+
|
|
100
|
+
if (
|
|
101
|
+
value === ',' &&
|
|
102
|
+
parens === 0 &&
|
|
103
|
+
braces === 0 &&
|
|
104
|
+
brackets === 0
|
|
105
|
+
) {
|
|
106
|
+
result.push(tokens.slice(start, i))
|
|
107
|
+
start = i + 1
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (start < tokens.length) result.push(tokens.slice(start))
|
|
112
|
+
return result
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function parseStage(source, tokens) {
|
|
116
|
+
if (!tokens.length) throw new SyntaxError('Empty pipeline stage')
|
|
117
|
+
|
|
118
|
+
if (tokens.every((token, index) =>
|
|
119
|
+
index % 2 === 0
|
|
120
|
+
? token.type === 'identifier'
|
|
121
|
+
: token.value === '.'
|
|
122
|
+
)) {
|
|
123
|
+
const name = text(source, tokens).trim()
|
|
124
|
+
if (/^[A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)*$/.test(name)) {
|
|
125
|
+
return { name, args: [] }
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let openIndex = -1
|
|
130
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
131
|
+
if (tokens[i].value === '(') {
|
|
132
|
+
openIndex = i
|
|
133
|
+
break
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (openIndex === -1) {
|
|
138
|
+
throw new SyntaxError(`Invalid pipeline stage: ${text(source, tokens)}`)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const closeIndex = matchingToken(tokens, openIndex, '(', ')')
|
|
142
|
+
if (closeIndex !== tokens.length - 1) {
|
|
143
|
+
throw new SyntaxError(`Invalid pipeline stage: ${text(source, tokens)}`)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const name = text(source, tokens.slice(0, openIndex)).trim()
|
|
147
|
+
|
|
148
|
+
if (!/^[A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)*$/.test(name)) {
|
|
149
|
+
throw new SyntaxError(`Invalid pipeline function: ${name}`)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const args = splitArguments(
|
|
153
|
+
tokens.slice(openIndex + 1, closeIndex)
|
|
154
|
+
).map(argument => text(source, argument).trim())
|
|
155
|
+
|
|
156
|
+
return { name, args }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function parsePipelineStatement(source, statementStart, statementEnd) {
|
|
160
|
+
const statement = source.slice(statementStart, statementEnd)
|
|
161
|
+
const tokens = significantTokens(tokenize(statement))
|
|
162
|
+
|
|
163
|
+
const firstPipeIndex = tokens.findIndex(token => token.type === 'pipeline')
|
|
164
|
+
if (firstPipeIndex === -1) return null
|
|
165
|
+
|
|
166
|
+
let inputStartIndex = 0
|
|
167
|
+
let parens = 0
|
|
168
|
+
let brackets = 0
|
|
169
|
+
let braces = 0
|
|
170
|
+
|
|
171
|
+
for (let i = 0; i < firstPipeIndex; i++) {
|
|
172
|
+
const token = tokens[i]
|
|
173
|
+
|
|
174
|
+
if (token.value === '(') { parens++; continue }
|
|
175
|
+
if (token.value === ')') { parens--; continue }
|
|
176
|
+
if (token.value === '[') { brackets++; continue }
|
|
177
|
+
if (token.value === ']') { brackets--; continue }
|
|
178
|
+
if (token.value === '{') { braces++; continue }
|
|
179
|
+
if (token.value === '}') { braces--; continue }
|
|
180
|
+
|
|
181
|
+
if (parens === 0 && brackets === 0 && braces === 0) {
|
|
182
|
+
if (token.value === '=' || token.value === '=>' || token.value === ':') {
|
|
183
|
+
inputStartIndex = i + 1
|
|
184
|
+
}
|
|
185
|
+
if (token.type === 'identifier' && token.value === 'return') {
|
|
186
|
+
inputStartIndex = i + 1
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const inputTokens = tokens.slice(inputStartIndex, firstPipeIndex)
|
|
192
|
+
if (!inputTokens.length) {
|
|
193
|
+
throw new SyntaxError(`Pipeline has no input expression:\n${statement}`)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const expression = text(statement, inputTokens).trim()
|
|
197
|
+
if (!expression) {
|
|
198
|
+
throw new SyntaxError(`Pipeline has no input expression:\n${statement}`)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const stages = []
|
|
202
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
203
|
+
if (tokens[i].type !== 'pipeline') continue
|
|
204
|
+
|
|
205
|
+
const stageStart = i + 1
|
|
206
|
+
let stageEnd = tokens.length
|
|
207
|
+
|
|
208
|
+
for (let j = stageStart; j < tokens.length; j++) {
|
|
209
|
+
if (tokens[j].type === 'pipeline') {
|
|
210
|
+
stageEnd = j
|
|
211
|
+
break
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const stageTokens = tokens.slice(stageStart, stageEnd)
|
|
216
|
+
if (!stageTokens.length) {
|
|
217
|
+
throw new SyntaxError(`Empty pipeline stage in:\n${statement}`)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
stages.push(parseStage(statement, stageTokens))
|
|
221
|
+
i = stageEnd - 1
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const prefixTokens = tokens.slice(0, inputStartIndex)
|
|
225
|
+
const prefix = prefixTokens.length ? text(statement, prefixTokens).trim() : ''
|
|
226
|
+
|
|
227
|
+
return { expression, stages, prefix }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function metaLiteral(pipelineId, nodeId, type, extra = {}) {
|
|
231
|
+
return JSON.stringify({ id: nodeId, pipelineId, type, ...extra })
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/*
|
|
235
|
+
* A bare integer shorthand (`retry(3)`) becomes `{ attempts: 3 }`; anything
|
|
236
|
+
* else (an object literal, a variable holding options, ...) is passed
|
|
237
|
+
* through untouched and assumed to already be options-shaped at runtime.
|
|
238
|
+
*/
|
|
239
|
+
function normalizeRetryOptions(args) {
|
|
240
|
+
if (args.length === 0) {
|
|
241
|
+
throw new SyntaxError('retry expects an attempt count or an options object, e.g. `retry(3)` or `retry({ attempts: 3 })`')
|
|
242
|
+
}
|
|
243
|
+
if (args.length > 1) {
|
|
244
|
+
throw new SyntaxError('retry takes exactly one argument: an attempt count or an options object')
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const arg = args[0].trim()
|
|
248
|
+
return /^\d+$/.test(arg) ? `{ attempts: ${arg} }` : arg
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/*
|
|
252
|
+
* `retry(...)` immediately following a `map`/`mapAsync`/`filterAsync`/
|
|
253
|
+
* `tap` stage fuses into that stage's function argument, so each *item* is
|
|
254
|
+
* retried independently (`map(items, retry(fn, opts, meta))`) instead of
|
|
255
|
+
* retry meaninglessly wrapping an already-lazy, not-yet-run generator.
|
|
256
|
+
* With no such preceding stage, `retry` instead wraps the whole expression
|
|
257
|
+
* built so far as a standalone stage (handled in the main emission loop).
|
|
258
|
+
*/
|
|
259
|
+
function fuseRetryStages(stages, pipelineId) {
|
|
260
|
+
const result = []
|
|
261
|
+
|
|
262
|
+
for (const stage of stages) {
|
|
263
|
+
if (stage.name !== 'retry') {
|
|
264
|
+
result.push(stage)
|
|
265
|
+
continue
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const options = normalizeRetryOptions(stage.args)
|
|
269
|
+
const previous = result[result.length - 1]
|
|
270
|
+
|
|
271
|
+
if (previous && MAP_LIKE_STAGES.has(previous.name) && previous.args.length) {
|
|
272
|
+
const meta = metaLiteral(pipelineId, stage.nodeId, 'retry')
|
|
273
|
+
const lastIndex = previous.args.length - 1
|
|
274
|
+
previous.args[lastIndex] = `retry(${previous.args[lastIndex]}, ${options}, ${meta})`
|
|
275
|
+
continue
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
result.push({ ...stage, args: [options], standaloneRetry: true })
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return result
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function emitPipeline(pipeline, options = {}) {
|
|
285
|
+
let expression = pipeline.expression.trim()
|
|
286
|
+
let prefix = pipeline.prefix ? `${pipeline.prefix} ` : ''
|
|
287
|
+
|
|
288
|
+
const pipelineId = computePipelineId(pipeline)
|
|
289
|
+
|
|
290
|
+
const checkpointOrder = pipeline.stages
|
|
291
|
+
.filter(stage => stage.name === 'checkpoint' && stage.args.length)
|
|
292
|
+
.map(stage => stripQuotes(stage.args[0]))
|
|
293
|
+
|
|
294
|
+
let checkpointsSeen = 0
|
|
295
|
+
|
|
296
|
+
const stagesWithIds = pipeline.stages.map((stage, index) => ({
|
|
297
|
+
...stage,
|
|
298
|
+
nodeId: `node-${index + 1}`
|
|
299
|
+
}))
|
|
300
|
+
|
|
301
|
+
const stages = fuseRetryStages(stagesWithIds, pipelineId)
|
|
302
|
+
|
|
303
|
+
if (options.profile) {
|
|
304
|
+
expression = `traceNode(${expression}, ${metaLiteral(pipelineId, 'node-0', 'source')})`
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
for (const stage of stages) {
|
|
308
|
+
if (PIPELINE_CONTROL_STAGES.has(stage.name)) {
|
|
309
|
+
expression = emitControlStage(stage, expression)
|
|
310
|
+
continue
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (stage.name === 'retry' && stage.standaloneRetry) {
|
|
314
|
+
const meta = metaLiteral(pipelineId, stage.nodeId, 'retry')
|
|
315
|
+
expression = `(await retry(async () => (${expression}), ${stage.args[0]}, ${meta})())`
|
|
316
|
+
if (options.profile) {
|
|
317
|
+
expression = `traceNode(${expression}, ${metaLiteral(pipelineId, stage.nodeId, 'retry')})`
|
|
318
|
+
}
|
|
319
|
+
continue
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const placeholderIndex = stage.args.indexOf('_')
|
|
323
|
+
|
|
324
|
+
if (placeholderIndex !== -1) {
|
|
325
|
+
const args = stage.args.slice()
|
|
326
|
+
args[placeholderIndex] = expression
|
|
327
|
+
expression = `${stage.name}(${args.join(', ')})`
|
|
328
|
+
} else if (stage.args.length === 0) {
|
|
329
|
+
expression = `${stage.name}(${expression})`
|
|
330
|
+
} else {
|
|
331
|
+
expression = `${stage.name}(${expression}, ${stage.args.join(', ')})`
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (RUNTIME_AWARE_STAGES.has(stage.name)) {
|
|
335
|
+
const extra = stage.name === 'checkpoint'
|
|
336
|
+
? { checkpointOrder, checkpointIndex: checkpointsSeen++ }
|
|
337
|
+
: {}
|
|
338
|
+
const meta = metaLiteral(pipelineId, stage.nodeId, stage.name, extra)
|
|
339
|
+
expression = expression.slice(0, -1) + `, ${meta})`
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (options.profile) {
|
|
343
|
+
expression = `traceNode(${expression}, ${metaLiteral(pipelineId, stage.nodeId, stage.name)})`
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return prefix + expression
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function stripQuotes(text) {
|
|
351
|
+
const trimmed = text.trim()
|
|
352
|
+
return /^["'`]/.test(trimmed) ? trimmed.slice(1, -1) : trimmed
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function emitControlStage(stage, expression) {
|
|
356
|
+
if (stage.name === 'await') {
|
|
357
|
+
if (stage.args.length) {
|
|
358
|
+
throw new SyntaxError('the `await` pipeline stage does not take arguments')
|
|
359
|
+
}
|
|
360
|
+
return `(await (${expression}))`
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (stage.name === 'catch') {
|
|
364
|
+
if (stage.args.length !== 1) {
|
|
365
|
+
throw new SyntaxError('the `catch` pipeline stage takes exactly one handler argument, e.g. `|> catch(handler)`')
|
|
366
|
+
}
|
|
367
|
+
return `(() => { try { return (${expression}) } catch (__error) { return (${stage.args[0]})(__error) } })()`
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (stage.name === 'orElse') {
|
|
371
|
+
if (stage.args.length !== 1) {
|
|
372
|
+
throw new SyntaxError('the `orElse` pipeline stage takes exactly one fallback argument, e.g. `|> orElse(fallback)`')
|
|
373
|
+
}
|
|
374
|
+
return `(() => { try { return (${expression}) } catch (__error) { return (${stage.args[0]}) } })()`
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
throw new SyntaxError(`Unknown pipeline control stage: ${stage.name}`)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function findStatementBounds(source, pipe) {
|
|
381
|
+
const mask = maskNonCode(source)
|
|
382
|
+
|
|
383
|
+
let start = pipe.start
|
|
384
|
+
let parenDepth = 0
|
|
385
|
+
let bracketDepth = 0
|
|
386
|
+
let braceDepth = 0
|
|
387
|
+
|
|
388
|
+
for (let i = pipe.start - 1; i >= 0; i--) {
|
|
389
|
+
const c = mask[i]
|
|
390
|
+
|
|
391
|
+
if (c === ')') { parenDepth++; continue }
|
|
392
|
+
if (c === ']') { bracketDepth++; continue }
|
|
393
|
+
if (c === '}') { braceDepth++; continue }
|
|
394
|
+
|
|
395
|
+
if (c === '(') {
|
|
396
|
+
if (parenDepth > 0) { parenDepth--; continue }
|
|
397
|
+
start = i + 1
|
|
398
|
+
break
|
|
399
|
+
}
|
|
400
|
+
if (c === '[') {
|
|
401
|
+
if (bracketDepth > 0) { bracketDepth--; continue }
|
|
402
|
+
start = i + 1
|
|
403
|
+
break
|
|
404
|
+
}
|
|
405
|
+
if (c === '{') {
|
|
406
|
+
if (braceDepth > 0) { braceDepth--; continue }
|
|
407
|
+
start = i + 1
|
|
408
|
+
break
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (parenDepth !== 0 || bracketDepth !== 0 || braceDepth !== 0) continue
|
|
412
|
+
|
|
413
|
+
if (c === ';' || c === ',') {
|
|
414
|
+
start = i + 1
|
|
415
|
+
break
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (c === '\n') {
|
|
419
|
+
let j = i + 1
|
|
420
|
+
while (j < pipe.start && [' ', '\t', '\r'].includes(mask[j])) j++
|
|
421
|
+
if (mask.slice(j, j + 2) !== '|>' && mask[j] !== '.') {
|
|
422
|
+
start = i + 1
|
|
423
|
+
break
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
start = i
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
while (
|
|
431
|
+
start < pipe.start &&
|
|
432
|
+
[' ', '\t', '\r', '\n'].includes(mask[start])
|
|
433
|
+
) start++
|
|
434
|
+
|
|
435
|
+
let end = source.length
|
|
436
|
+
parenDepth = 0
|
|
437
|
+
bracketDepth = 0
|
|
438
|
+
braceDepth = 0
|
|
439
|
+
|
|
440
|
+
for (let i = pipe.end; i < source.length; i++) {
|
|
441
|
+
const c = mask[i]
|
|
442
|
+
|
|
443
|
+
if (c === '(') { parenDepth++; continue }
|
|
444
|
+
if (c === ')') {
|
|
445
|
+
if (parenDepth > 0) { parenDepth--; continue }
|
|
446
|
+
end = i
|
|
447
|
+
break
|
|
448
|
+
}
|
|
449
|
+
if (c === '[') { bracketDepth++; continue }
|
|
450
|
+
if (c === ']') {
|
|
451
|
+
if (bracketDepth > 0) { bracketDepth--; continue }
|
|
452
|
+
end = i
|
|
453
|
+
break
|
|
454
|
+
}
|
|
455
|
+
if (c === '{') { braceDepth++; continue }
|
|
456
|
+
if (c === '}') {
|
|
457
|
+
if (braceDepth > 0) { braceDepth--; continue }
|
|
458
|
+
end = i
|
|
459
|
+
break
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (
|
|
463
|
+
parenDepth === 0 &&
|
|
464
|
+
bracketDepth === 0 &&
|
|
465
|
+
braceDepth === 0 &&
|
|
466
|
+
(c === ';' || c === ',')
|
|
467
|
+
) {
|
|
468
|
+
end = i
|
|
469
|
+
break
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (
|
|
473
|
+
parenDepth === 0 &&
|
|
474
|
+
bracketDepth === 0 &&
|
|
475
|
+
braceDepth === 0 &&
|
|
476
|
+
c === '\n'
|
|
477
|
+
) {
|
|
478
|
+
let j = i + 1
|
|
479
|
+
while (
|
|
480
|
+
j < source.length &&
|
|
481
|
+
[' ', '\t', '\r'].includes(mask[j])
|
|
482
|
+
) j++
|
|
483
|
+
|
|
484
|
+
if (mask.slice(j, j + 2) === '|>') continue
|
|
485
|
+
end = i
|
|
486
|
+
break
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return { start, end }
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export function transformPipelines(source, options = {}) {
|
|
494
|
+
let result = source
|
|
495
|
+
|
|
496
|
+
for (let iteration = 0; iteration < 1000; iteration++) {
|
|
497
|
+
const tokens = significantTokens(tokenize(result))
|
|
498
|
+
const pipes = tokens.filter(token => token.type === 'pipeline')
|
|
499
|
+
|
|
500
|
+
if (!pipes.length) return result
|
|
501
|
+
|
|
502
|
+
const pipe = pipes[pipes.length - 1]
|
|
503
|
+
const bounds = findStatementBounds(result, pipe)
|
|
504
|
+
const pipeline = parsePipelineStatement(result, bounds.start, bounds.end)
|
|
505
|
+
|
|
506
|
+
if (!pipeline) {
|
|
507
|
+
throw new SyntaxError(
|
|
508
|
+
`Unable to parse pipeline:\n\n${result.slice(bounds.start, bounds.end)}`
|
|
509
|
+
)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const replacement = emitPipeline(pipeline, options)
|
|
513
|
+
|
|
514
|
+
result =
|
|
515
|
+
result.slice(0, bounds.start) +
|
|
516
|
+
replacement +
|
|
517
|
+
result.slice(bounds.end)
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
throw new SyntaxError('Pipeline transformation exceeded 1000 iterations')
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export function findPipelineStdlibFunctions(source) {
|
|
524
|
+
const tokens = significantTokens(tokenize(source))
|
|
525
|
+
const result = new Set()
|
|
526
|
+
|
|
527
|
+
for (let i = 0; i < tokens.length - 1; i++) {
|
|
528
|
+
if (
|
|
529
|
+
tokens[i].type === 'pipeline' &&
|
|
530
|
+
tokens[i + 1].type === 'identifier' &&
|
|
531
|
+
PIPELINE_STDLIB.has(tokens[i + 1].value)
|
|
532
|
+
) {
|
|
533
|
+
result.add(tokens[i + 1].value)
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
return [...result]
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/*
|
|
541
|
+
* True if the source contains at least one pipeline statement. Used by the
|
|
542
|
+
* compiler to decide whether the `traceNode` profiling helper needs to be
|
|
543
|
+
* auto-imported, independent of which (if any) stdlib stages are used.
|
|
544
|
+
*/
|
|
545
|
+
export function hasPipelines(source) {
|
|
546
|
+
const tokens = significantTokens(tokenize(source))
|
|
547
|
+
return tokens.some(token => token.type === 'pipeline')
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/*
|
|
551
|
+
* Non-mutating forward scan collecting every pipeline statement in the
|
|
552
|
+
* source, used for static analysis (`jojo graph`) rather than compilation.
|
|
553
|
+
* Unlike `transformPipelines` (which repeatedly processes the *last* pipe
|
|
554
|
+
* in the whole file so nested pipelines-in-arguments resolve inside-out),
|
|
555
|
+
* this simply walks forward statement-by-statement, which is sufficient
|
|
556
|
+
* for the common case of top-level `|>` chains that graph/profile display.
|
|
557
|
+
*/
|
|
558
|
+
export function findAllPipelines(source) {
|
|
559
|
+
const pipelines = []
|
|
560
|
+
let searchFrom = 0
|
|
561
|
+
|
|
562
|
+
while (searchFrom < source.length) {
|
|
563
|
+
const remainder = source.slice(searchFrom)
|
|
564
|
+
const tokens = significantTokens(tokenize(remainder))
|
|
565
|
+
const pipe = tokens.find(token => token.type === 'pipeline')
|
|
566
|
+
if (!pipe) break
|
|
567
|
+
|
|
568
|
+
const absolutePipe = { ...pipe, start: pipe.start + searchFrom, end: pipe.end + searchFrom }
|
|
569
|
+
const bounds = findStatementBounds(source, absolutePipe)
|
|
570
|
+
const pipeline = parsePipelineStatement(source, bounds.start, bounds.end)
|
|
571
|
+
|
|
572
|
+
if (pipeline) {
|
|
573
|
+
pipelines.push({ ...pipeline, start: bounds.start, end: bounds.end })
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
searchFrom = Math.max(bounds.end, absolutePipe.end) + 1
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return pipelines
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
export { parsePipelineStatement, findStatementBounds }
|
|
583
|
+
|