@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.
- package/LICENSE +24 -0
- package/README.md +418 -0
- package/bin/jojo.js +170 -0
- package/package.json +59 -0
- package/src/compiler.js +169 -0
- package/src/index.js +19 -0
- package/src/lexer.js +178 -0
- package/src/match-parser.js +215 -0
- package/src/pipeline-parser.js +418 -0
- package/src/runtime/collections.js +327 -0
- package/src/runtime/index.js +29 -0
- package/src/transforms.js +281 -0
- package/src/type-annotations.js +191 -0
|
@@ -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 }
|