@panagos/jojoscript 0.7.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.
@@ -0,0 +1,418 @@
1
+ import {
2
+ tokenize,
3
+ significantTokens,
4
+ maskNonCode
5
+ } from './lexer.js'
6
+
7
+ const PIPELINE_STDLIB = new Set([
8
+ 'map',
9
+ 'filter',
10
+ 'sort',
11
+ 'unique',
12
+ 'take',
13
+ 'skip',
14
+ 'flatMap',
15
+ 'reduce',
16
+ 'toArray',
17
+ 'count',
18
+ 'first',
19
+ 'find',
20
+ 'some',
21
+ 'every',
22
+ 'groupBy',
23
+ 'partition',
24
+ 'chunk',
25
+ 'window',
26
+ 'zip',
27
+ 'zipWith',
28
+ 'scan',
29
+ 'sortBy',
30
+ 'distinctBy',
31
+ 'tap',
32
+ 'mapAsync',
33
+ 'filterAsync',
34
+ 'toArrayAsync'
35
+ ])
36
+
37
+ /*
38
+ * These stage names are handled at compile time instead of being resolved
39
+ * to a runtime function call, so they must never be added as automatic
40
+ * `jojoscript/runtime` imports.
41
+ */
42
+ const PIPELINE_CONTROL_STAGES = new Set(['await', 'catch', 'orElse'])
43
+
44
+ function text(source, tokens) {
45
+ if (!tokens.length) return ''
46
+ return source.slice(tokens[0].start, tokens[tokens.length - 1].end)
47
+ }
48
+
49
+ function matchingToken(tokens, start, open, close) {
50
+ let depth = 0
51
+ for (let i = start; i < tokens.length; i++) {
52
+ if (tokens[i].value === open) depth++
53
+ if (tokens[i].value === close) {
54
+ depth--
55
+ if (depth === 0) return i
56
+ }
57
+ }
58
+ throw new SyntaxError(`Unclosed ${open}`)
59
+ }
60
+
61
+ function splitArguments(tokens) {
62
+ const result = []
63
+ let start = 0
64
+ let parens = 0
65
+ let braces = 0
66
+ let brackets = 0
67
+
68
+ for (let i = 0; i < tokens.length; i++) {
69
+ const value = tokens[i].value
70
+ if (value === '(') parens++
71
+ else if (value === ')') parens--
72
+ else if (value === '{') braces++
73
+ else if (value === '}') braces--
74
+ else if (value === '[') brackets++
75
+ else if (value === ']') brackets--
76
+
77
+ if (
78
+ value === ',' &&
79
+ parens === 0 &&
80
+ braces === 0 &&
81
+ brackets === 0
82
+ ) {
83
+ result.push(tokens.slice(start, i))
84
+ start = i + 1
85
+ }
86
+ }
87
+
88
+ if (start < tokens.length) result.push(tokens.slice(start))
89
+ return result
90
+ }
91
+
92
+ function parseStage(source, tokens) {
93
+ if (!tokens.length) throw new SyntaxError('Empty pipeline stage')
94
+
95
+ if (tokens.every((token, index) =>
96
+ index % 2 === 0
97
+ ? token.type === 'identifier'
98
+ : token.value === '.'
99
+ )) {
100
+ const name = text(source, tokens).trim()
101
+ if (/^[A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)*$/.test(name)) {
102
+ return { name, args: [] }
103
+ }
104
+ }
105
+
106
+ let openIndex = -1
107
+ for (let i = 0; i < tokens.length; i++) {
108
+ if (tokens[i].value === '(') {
109
+ openIndex = i
110
+ break
111
+ }
112
+ }
113
+
114
+ if (openIndex === -1) {
115
+ throw new SyntaxError(`Invalid pipeline stage: ${text(source, tokens)}`)
116
+ }
117
+
118
+ const closeIndex = matchingToken(tokens, openIndex, '(', ')')
119
+ if (closeIndex !== tokens.length - 1) {
120
+ throw new SyntaxError(`Invalid pipeline stage: ${text(source, tokens)}`)
121
+ }
122
+
123
+ const name = text(source, tokens.slice(0, openIndex)).trim()
124
+
125
+ if (!/^[A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)*$/.test(name)) {
126
+ throw new SyntaxError(`Invalid pipeline function: ${name}`)
127
+ }
128
+
129
+ const args = splitArguments(
130
+ tokens.slice(openIndex + 1, closeIndex)
131
+ ).map(argument => text(source, argument).trim())
132
+
133
+ return { name, args }
134
+ }
135
+
136
+ function parsePipelineStatement(source, statementStart, statementEnd) {
137
+ const statement = source.slice(statementStart, statementEnd)
138
+ const tokens = significantTokens(tokenize(statement))
139
+
140
+ const firstPipeIndex = tokens.findIndex(token => token.type === 'pipeline')
141
+ if (firstPipeIndex === -1) return null
142
+
143
+ let inputStartIndex = 0
144
+ let parens = 0
145
+ let brackets = 0
146
+ let braces = 0
147
+
148
+ for (let i = 0; i < firstPipeIndex; i++) {
149
+ const token = tokens[i]
150
+
151
+ if (token.value === '(') { parens++; continue }
152
+ if (token.value === ')') { parens--; continue }
153
+ if (token.value === '[') { brackets++; continue }
154
+ if (token.value === ']') { brackets--; continue }
155
+ if (token.value === '{') { braces++; continue }
156
+ if (token.value === '}') { braces--; continue }
157
+
158
+ if (parens === 0 && brackets === 0 && braces === 0) {
159
+ if (token.value === '=' || token.value === '=>' || token.value === ':') {
160
+ inputStartIndex = i + 1
161
+ }
162
+ if (token.type === 'identifier' && token.value === 'return') {
163
+ inputStartIndex = i + 1
164
+ }
165
+ }
166
+ }
167
+
168
+ const inputTokens = tokens.slice(inputStartIndex, firstPipeIndex)
169
+ if (!inputTokens.length) {
170
+ throw new SyntaxError(`Pipeline has no input expression:\n${statement}`)
171
+ }
172
+
173
+ const expression = text(statement, inputTokens).trim()
174
+ if (!expression) {
175
+ throw new SyntaxError(`Pipeline has no input expression:\n${statement}`)
176
+ }
177
+
178
+ const stages = []
179
+ for (let i = 0; i < tokens.length; i++) {
180
+ if (tokens[i].type !== 'pipeline') continue
181
+
182
+ const stageStart = i + 1
183
+ let stageEnd = tokens.length
184
+
185
+ for (let j = stageStart; j < tokens.length; j++) {
186
+ if (tokens[j].type === 'pipeline') {
187
+ stageEnd = j
188
+ break
189
+ }
190
+ }
191
+
192
+ const stageTokens = tokens.slice(stageStart, stageEnd)
193
+ if (!stageTokens.length) {
194
+ throw new SyntaxError(`Empty pipeline stage in:\n${statement}`)
195
+ }
196
+
197
+ stages.push(parseStage(statement, stageTokens))
198
+ i = stageEnd - 1
199
+ }
200
+
201
+ const prefixTokens = tokens.slice(0, inputStartIndex)
202
+ const prefix = prefixTokens.length ? text(statement, prefixTokens).trim() : ''
203
+
204
+ return { expression, stages, prefix }
205
+ }
206
+
207
+ export function emitPipeline(pipeline) {
208
+ let expression = pipeline.expression.trim()
209
+ let prefix = pipeline.prefix ? `${pipeline.prefix} ` : ''
210
+
211
+ for (const stage of pipeline.stages) {
212
+ if (PIPELINE_CONTROL_STAGES.has(stage.name)) {
213
+ expression = emitControlStage(stage, expression)
214
+ continue
215
+ }
216
+
217
+ const placeholderIndex = stage.args.indexOf('_')
218
+
219
+ if (placeholderIndex !== -1) {
220
+ const args = stage.args.slice()
221
+ args[placeholderIndex] = expression
222
+ expression = `${stage.name}(${args.join(', ')})`
223
+ } else if (stage.args.length === 0) {
224
+ expression = `${stage.name}(${expression})`
225
+ } else {
226
+ expression = `${stage.name}(${expression}, ${stage.args.join(', ')})`
227
+ }
228
+ }
229
+
230
+ return prefix + expression
231
+ }
232
+
233
+ function emitControlStage(stage, expression) {
234
+ if (stage.name === 'await') {
235
+ if (stage.args.length) {
236
+ throw new SyntaxError('the `await` pipeline stage does not take arguments')
237
+ }
238
+ return `(await (${expression}))`
239
+ }
240
+
241
+ if (stage.name === 'catch') {
242
+ if (stage.args.length !== 1) {
243
+ throw new SyntaxError('the `catch` pipeline stage takes exactly one handler argument, e.g. `|> catch(handler)`')
244
+ }
245
+ return `(() => { try { return (${expression}) } catch (__error) { return (${stage.args[0]})(__error) } })()`
246
+ }
247
+
248
+ if (stage.name === 'orElse') {
249
+ if (stage.args.length !== 1) {
250
+ throw new SyntaxError('the `orElse` pipeline stage takes exactly one fallback argument, e.g. `|> orElse(fallback)`')
251
+ }
252
+ return `(() => { try { return (${expression}) } catch (__error) { return (${stage.args[0]}) } })()`
253
+ }
254
+
255
+ throw new SyntaxError(`Unknown pipeline control stage: ${stage.name}`)
256
+ }
257
+
258
+ function findStatementBounds(source, pipe) {
259
+ const mask = maskNonCode(source)
260
+
261
+ let start = pipe.start
262
+ let parenDepth = 0
263
+ let bracketDepth = 0
264
+ let braceDepth = 0
265
+
266
+ for (let i = pipe.start - 1; i >= 0; i--) {
267
+ const c = mask[i]
268
+
269
+ if (c === ')') { parenDepth++; continue }
270
+ if (c === ']') { bracketDepth++; continue }
271
+ if (c === '}') { braceDepth++; continue }
272
+
273
+ if (c === '(') {
274
+ if (parenDepth > 0) { parenDepth--; continue }
275
+ start = i + 1
276
+ break
277
+ }
278
+ if (c === '[') {
279
+ if (bracketDepth > 0) { bracketDepth--; continue }
280
+ start = i + 1
281
+ break
282
+ }
283
+ if (c === '{') {
284
+ if (braceDepth > 0) { braceDepth--; continue }
285
+ start = i + 1
286
+ break
287
+ }
288
+
289
+ if (parenDepth !== 0 || bracketDepth !== 0 || braceDepth !== 0) continue
290
+
291
+ if (c === ';' || c === ',') {
292
+ start = i + 1
293
+ break
294
+ }
295
+
296
+ if (c === '\n') {
297
+ let j = i - 1
298
+ while (j >= 0 && [' ', '\t', '\r'].includes(mask[j])) j--
299
+ if (j < 0 || mask[j] === '\n') {
300
+ start = i + 1
301
+ break
302
+ }
303
+ }
304
+
305
+ start = i
306
+ }
307
+
308
+ while (
309
+ start < pipe.start &&
310
+ [' ', '\t', '\r', '\n'].includes(mask[start])
311
+ ) start++
312
+
313
+ let end = source.length
314
+ parenDepth = 0
315
+ bracketDepth = 0
316
+ braceDepth = 0
317
+
318
+ for (let i = pipe.end; i < source.length; i++) {
319
+ const c = mask[i]
320
+
321
+ if (c === '(') { parenDepth++; continue }
322
+ if (c === ')') {
323
+ if (parenDepth > 0) { parenDepth--; continue }
324
+ end = i
325
+ break
326
+ }
327
+ if (c === '[') { bracketDepth++; continue }
328
+ if (c === ']') {
329
+ if (bracketDepth > 0) { bracketDepth--; continue }
330
+ end = i
331
+ break
332
+ }
333
+ if (c === '{') { braceDepth++; continue }
334
+ if (c === '}') {
335
+ if (braceDepth > 0) { braceDepth--; continue }
336
+ end = i
337
+ break
338
+ }
339
+
340
+ if (
341
+ parenDepth === 0 &&
342
+ bracketDepth === 0 &&
343
+ braceDepth === 0 &&
344
+ (c === ';' || c === ',')
345
+ ) {
346
+ end = i
347
+ break
348
+ }
349
+
350
+ if (
351
+ parenDepth === 0 &&
352
+ bracketDepth === 0 &&
353
+ braceDepth === 0 &&
354
+ c === '\n'
355
+ ) {
356
+ let j = i + 1
357
+ while (
358
+ j < source.length &&
359
+ [' ', '\t', '\r'].includes(mask[j])
360
+ ) j++
361
+
362
+ if (mask.slice(j, j + 2) === '|>') continue
363
+ end = i
364
+ break
365
+ }
366
+ }
367
+
368
+ return { start, end }
369
+ }
370
+
371
+ export function transformPipelines(source) {
372
+ let result = source
373
+
374
+ for (let iteration = 0; iteration < 1000; iteration++) {
375
+ const tokens = significantTokens(tokenize(result))
376
+ const pipes = tokens.filter(token => token.type === 'pipeline')
377
+
378
+ if (!pipes.length) return result
379
+
380
+ const pipe = pipes[pipes.length - 1]
381
+ const bounds = findStatementBounds(result, pipe)
382
+ const pipeline = parsePipelineStatement(result, bounds.start, bounds.end)
383
+
384
+ if (!pipeline) {
385
+ throw new SyntaxError(
386
+ `Unable to parse pipeline:\n\n${result.slice(bounds.start, bounds.end)}`
387
+ )
388
+ }
389
+
390
+ const replacement = emitPipeline(pipeline)
391
+
392
+ result =
393
+ result.slice(0, bounds.start) +
394
+ replacement +
395
+ result.slice(bounds.end)
396
+ }
397
+
398
+ throw new SyntaxError('Pipeline transformation exceeded 1000 iterations')
399
+ }
400
+
401
+ export function findPipelineStdlibFunctions(source) {
402
+ const tokens = significantTokens(tokenize(source))
403
+ const result = new Set()
404
+
405
+ for (let i = 0; i < tokens.length - 1; i++) {
406
+ if (
407
+ tokens[i].type === 'pipeline' &&
408
+ tokens[i + 1].type === 'identifier' &&
409
+ PIPELINE_STDLIB.has(tokens[i + 1].value)
410
+ ) {
411
+ result.add(tokens[i + 1].value)
412
+ }
413
+ }
414
+
415
+ return [...result]
416
+ }
417
+
418
+ export { parsePipelineStatement, findStatementBounds }