@panagos/jojoscript 0.7.1 → 0.9.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 +809 -418
- package/bin/jojo.js +430 -170
- package/package.json +50 -59
- package/src/compiler.js +227 -186
- package/src/effect-parser.js +223 -0
- package/src/index.js +14 -0
- package/src/pipeline-hash.js +27 -0
- package/src/pipeline-ir.js +157 -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 +467 -327
- package/src/runtime/effect-log.js +90 -0
- package/src/runtime/effects.js +263 -0
- package/src/runtime/errors.js +29 -0
- package/src/runtime/execution-context.js +86 -0
- package/src/runtime/index.js +70 -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/package.json
CHANGED
|
@@ -1,59 +1,50 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@panagos/jojoscript",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "A tiny JavaScript-compatible language layer with Jojo syntax and pipelines.",
|
|
5
|
-
"license": "Unlicense",
|
|
6
|
-
"type": "module",
|
|
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
|
-
"javascript",
|
|
52
|
-
"language",
|
|
53
|
-
"compiler",
|
|
54
|
-
"pipeline",
|
|
55
|
-
"functional-programming",
|
|
56
|
-
"iterators",
|
|
57
|
-
"lazy-iteration"
|
|
58
|
-
]
|
|
59
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@panagos/jojoscript",
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "A tiny JavaScript-compatible language layer with Jojo syntax and pipelines.",
|
|
5
|
+
"license": "Unlicense",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"jojo": "./bin/jojo.js"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": "./src/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./runtime": {
|
|
15
|
+
"import": "./src/runtime/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"bin/",
|
|
20
|
+
"src/",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"test": "node --test test/*.test.js",
|
|
29
|
+
"build:example": "npm --prefix example install && npm --prefix example run build",
|
|
30
|
+
"run:example": "npm --prefix example run start"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/panagos/jojoscript.git"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/panagos/jojoscript",
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/panagos/jojoscript/issues"
|
|
39
|
+
},
|
|
40
|
+
"keywords": [
|
|
41
|
+
"jojoscript",
|
|
42
|
+
"javascript",
|
|
43
|
+
"language",
|
|
44
|
+
"compiler",
|
|
45
|
+
"pipeline",
|
|
46
|
+
"functional-programming",
|
|
47
|
+
"iterators",
|
|
48
|
+
"lazy-iteration"
|
|
49
|
+
]
|
|
50
|
+
}
|
package/src/compiler.js
CHANGED
|
@@ -1,186 +1,227 @@
|
|
|
1
|
-
import fs from 'node:fs'
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
import { fileURLToPath } from 'node:url'
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
transformPipelines,
|
|
7
|
-
findPipelineStdlibFunctions
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
import {
|
|
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
|
-
result =
|
|
62
|
-
|
|
63
|
-
/*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
result =
|
|
74
|
-
|
|
75
|
-
/*
|
|
76
|
-
*
|
|
77
|
-
*/
|
|
78
|
-
result =
|
|
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
|
-
return {
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
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
|
-
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
transformPipelines,
|
|
7
|
+
findPipelineStdlibFunctions,
|
|
8
|
+
hasPipelines
|
|
9
|
+
} from './pipeline-parser.js'
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
transformMutableDeclarations,
|
|
13
|
+
transformConstDeclarations,
|
|
14
|
+
transformMultiBindingDeclarations,
|
|
15
|
+
transformDestructuringDeclarations,
|
|
16
|
+
transformFunctionDeclarations,
|
|
17
|
+
transformExpressionFunctions,
|
|
18
|
+
transformLocalImports
|
|
19
|
+
} from './transforms.js'
|
|
20
|
+
|
|
21
|
+
import { transformMatchExpressions } from './match-parser.js'
|
|
22
|
+
import { transformTypeAnnotations } from './type-annotations.js'
|
|
23
|
+
import {
|
|
24
|
+
transformEffectDeclarations,
|
|
25
|
+
transformHandleBlocks,
|
|
26
|
+
findEffectDeclarations,
|
|
27
|
+
findHandleBlocks
|
|
28
|
+
} from './effect-parser.js'
|
|
29
|
+
|
|
30
|
+
/*
|
|
31
|
+
* The auto-generated runtime import must use whatever name this package is
|
|
32
|
+
* actually installed under (e.g. a scoped fork like "@me/jojoscript"), not a
|
|
33
|
+
* hardcoded literal, so it's derived from this package's own package.json
|
|
34
|
+
* instead of being hardcoded.
|
|
35
|
+
*/
|
|
36
|
+
const PACKAGE_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
37
|
+
const OWN_PACKAGE_NAME = JSON.parse(
|
|
38
|
+
fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf8')
|
|
39
|
+
).name
|
|
40
|
+
const JOJO_RUNTIME = `${OWN_PACKAGE_NAME}/runtime`
|
|
41
|
+
|
|
42
|
+
export function compile(source, options = {}) {
|
|
43
|
+
let result = source
|
|
44
|
+
const runtimeImport = options.runtimeImport ?? JOJO_RUNTIME
|
|
45
|
+
|
|
46
|
+
/*
|
|
47
|
+
* Optional type annotations, stripped and re-expressed as JSDoc before
|
|
48
|
+
* anything else touches `fn` declarations.
|
|
49
|
+
*/
|
|
50
|
+
result = transformTypeAnnotations(result)
|
|
51
|
+
|
|
52
|
+
/*
|
|
53
|
+
* Jojo functions.
|
|
54
|
+
*/
|
|
55
|
+
result = transformExpressionFunctions(result)
|
|
56
|
+
result = transformFunctionDeclarations(result)
|
|
57
|
+
|
|
58
|
+
/*
|
|
59
|
+
* `match` expressions.
|
|
60
|
+
*/
|
|
61
|
+
result = transformMatchExpressions(result)
|
|
62
|
+
|
|
63
|
+
/*
|
|
64
|
+
* Effects: `effect name(params)` declarations and `handle { ... } { ... }`
|
|
65
|
+
* blocks. Detected before either transform runs (both remove the source
|
|
66
|
+
* syntax they recognize), so the compiler knows afterwards whether the
|
|
67
|
+
* `defineEffect`/`withHandlers` runtime helpers need to be auto-imported.
|
|
68
|
+
*/
|
|
69
|
+
const usesEffectDeclarations = findEffectDeclarations(result).length > 0
|
|
70
|
+
const usesHandleBlocks = findHandleBlocks(result).length > 0
|
|
71
|
+
|
|
72
|
+
result = transformEffectDeclarations(result)
|
|
73
|
+
result = transformHandleBlocks(result)
|
|
74
|
+
|
|
75
|
+
/*
|
|
76
|
+
* Jojo declarations.
|
|
77
|
+
*/
|
|
78
|
+
result = transformMultiBindingDeclarations(result)
|
|
79
|
+
result = transformDestructuringDeclarations(result)
|
|
80
|
+
result = transformMutableDeclarations(result)
|
|
81
|
+
result = transformConstDeclarations(result)
|
|
82
|
+
|
|
83
|
+
/*
|
|
84
|
+
* Remember which pipeline stages need the Jojo runtime before
|
|
85
|
+
* the pipeline syntax itself is removed.
|
|
86
|
+
*/
|
|
87
|
+
const pipelineStdlibFunctions =
|
|
88
|
+
findPipelineStdlibFunctions(result)
|
|
89
|
+
|
|
90
|
+
/*
|
|
91
|
+
* `effect`/`handle` syntax is never written by the user as a runtime
|
|
92
|
+
* function call, so their required runtime helpers are added explicitly,
|
|
93
|
+
* the same way `traceNode` is added below for `jojo profile`.
|
|
94
|
+
*/
|
|
95
|
+
if (usesEffectDeclarations && !pipelineStdlibFunctions.includes('defineEffect')) {
|
|
96
|
+
pipelineStdlibFunctions.push('defineEffect')
|
|
97
|
+
}
|
|
98
|
+
if (usesHandleBlocks && !pipelineStdlibFunctions.includes('withHandlers')) {
|
|
99
|
+
pipelineStdlibFunctions.push('withHandlers')
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/*
|
|
103
|
+
* `jojo profile` compiles with `{ profile: true }`, which wraps every
|
|
104
|
+
* pipeline stage (and the initial source expression) in a call to the
|
|
105
|
+
* `traceNode` runtime helper. That helper is never written by the user,
|
|
106
|
+
* so it must be added to the auto-import list explicitly.
|
|
107
|
+
*/
|
|
108
|
+
if (options.profile && hasPipelines(result) && !pipelineStdlibFunctions.includes('traceNode')) {
|
|
109
|
+
pipelineStdlibFunctions.push('traceNode')
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/*
|
|
113
|
+
* Pipelines.
|
|
114
|
+
*/
|
|
115
|
+
result = transformPipelines(result, options)
|
|
116
|
+
|
|
117
|
+
/*
|
|
118
|
+
* ES module imports.
|
|
119
|
+
*/
|
|
120
|
+
result = transformLocalImports(result)
|
|
121
|
+
|
|
122
|
+
/*
|
|
123
|
+
* Generated JavaScript imports the runtime as a normal package.
|
|
124
|
+
* This makes compiled applications portable and avoids globals. If the
|
|
125
|
+
* source already has a manual `jojoscript/runtime` import (e.g. for a
|
|
126
|
+
* stdlib function that is only ever called directly, never through a
|
|
127
|
+
* pipeline stage), the auto-imported names are merged into it instead
|
|
128
|
+
* of creating a second, colliding import declaration.
|
|
129
|
+
*/
|
|
130
|
+
if (pipelineStdlibFunctions.length) {
|
|
131
|
+
result = mergeRuntimeImport(result, pipelineStdlibFunctions, runtimeImport)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return result.endsWith('\n') ? result : result + '\n'
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function escapeRegExp(value) {
|
|
138
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function mergeRuntimeImport(source, stdlibFunctions, runtimeImport) {
|
|
142
|
+
const pattern = new RegExp(
|
|
143
|
+
`import\\s*\\{([^}]*)\\}\\s*from\\s*(["'])([^"']+\\/runtime)\\2`
|
|
144
|
+
)
|
|
145
|
+
const existing = source.match(pattern)
|
|
146
|
+
|
|
147
|
+
if (!existing) {
|
|
148
|
+
return `import { ${stdlibFunctions.join(', ')} } from "${runtimeImport}"\n` + source
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const existingNames = existing[1].split(',').map(name => name.trim()).filter(Boolean)
|
|
152
|
+
const merged = [...new Set([...existingNames, ...stdlibFunctions])]
|
|
153
|
+
|
|
154
|
+
return source.replace(
|
|
155
|
+
pattern,
|
|
156
|
+
`import { ${merged.join(', ')} } from "${runtimeImport}"`
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function compileFile(input, outDir = null, options = {}) {
|
|
161
|
+
const src = path.resolve(input)
|
|
162
|
+
const dir = outDir
|
|
163
|
+
? path.resolve(outDir)
|
|
164
|
+
: path.join(path.dirname(src), 'dist')
|
|
165
|
+
|
|
166
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
167
|
+
|
|
168
|
+
const out = path.join(
|
|
169
|
+
dir,
|
|
170
|
+
path.basename(src, path.extname(src)) + '.js'
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
fs.writeFileSync(
|
|
174
|
+
out,
|
|
175
|
+
compile(fs.readFileSync(src, 'utf8'), options),
|
|
176
|
+
'utf8'
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
return { input: src, output: out }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function compileDirectory(inputDir, outDir = null, options = {}) {
|
|
183
|
+
const root = path.resolve(inputDir)
|
|
184
|
+
const target = outDir
|
|
185
|
+
? path.resolve(outDir)
|
|
186
|
+
: path.join(root, 'dist')
|
|
187
|
+
|
|
188
|
+
const result = []
|
|
189
|
+
|
|
190
|
+
function walk(dir) {
|
|
191
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
192
|
+
if (
|
|
193
|
+
entry.name === 'node_modules' ||
|
|
194
|
+
entry.name === 'dist' ||
|
|
195
|
+
entry.name.startsWith('.')
|
|
196
|
+
) {
|
|
197
|
+
continue
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const full = path.join(dir, entry.name)
|
|
201
|
+
|
|
202
|
+
if (entry.isDirectory()) {
|
|
203
|
+
walk(full)
|
|
204
|
+
continue
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (!entry.name.endsWith('.jojo')) continue
|
|
208
|
+
|
|
209
|
+
const rel =
|
|
210
|
+
path.relative(root, full).slice(0, -'.jojo'.length) + '.js'
|
|
211
|
+
|
|
212
|
+
const out = path.join(target, rel)
|
|
213
|
+
|
|
214
|
+
fs.mkdirSync(path.dirname(out), { recursive: true })
|
|
215
|
+
fs.writeFileSync(
|
|
216
|
+
out,
|
|
217
|
+
compile(fs.readFileSync(full, 'utf8'), options),
|
|
218
|
+
'utf8'
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
result.push({ input: full, output: out })
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
walk(root)
|
|
226
|
+
return result
|
|
227
|
+
}
|