@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/package.json CHANGED
@@ -1,59 +1,50 @@
1
- {
2
- "name": "@panagos/jojoscript",
3
- "version": "0.7.0",
4
- "description": "A tiny JavaScript-compatible language layer with Jojo syntax and pipelines.",
5
- "license": "Unlicense",
6
- "type": "module",
7
-
8
- "bin": {
9
- "jojo": "./bin/jojo.js"
10
- },
11
-
12
- "exports": {
13
- ".": {
14
- "import": "./src/index.js"
15
- },
16
- "./runtime": {
17
- "import": "./src/runtime/index.js"
18
- }
19
- },
20
-
21
- "files": [
22
- "bin/",
23
- "src/",
24
- "README.md",
25
- "LICENSE"
26
- ],
27
-
28
- "engines": {
29
- "node": ">=18"
30
- },
31
-
32
- "scripts": {
33
- "test": "node --test test/*.test.js",
34
- "build:example": "npm --prefix example install && npm --prefix example run build",
35
- "run:example": "npm --prefix example run start"
36
- },
37
-
38
- "repository": {
39
- "type": "git",
40
- "url": "https://github.com/panagos/jojoscript.git"
41
- },
42
-
43
- "homepage": "https://github.com/panagos/jojoscript",
44
-
45
- "bugs": {
46
- "url": "https://github.com/panagos/jojoscript/issues"
47
- },
48
-
49
- "keywords": [
50
- "jojoscript",
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.8.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,169 +1,197 @@
1
- import fs from 'node:fs'
2
- import path from 'node:path'
3
-
4
- import {
5
- transformPipelines,
6
- findPipelineStdlibFunctions
7
- } from './pipeline-parser.js'
8
-
9
- import {
10
- transformMutableDeclarations,
11
- transformConstDeclarations,
12
- transformMultiBindingDeclarations,
13
- transformDestructuringDeclarations,
14
- transformFunctionDeclarations,
15
- transformExpressionFunctions,
16
- transformLocalImports
17
- } from './transforms.js'
18
-
19
- import { transformMatchExpressions } from './match-parser.js'
20
- import { transformTypeAnnotations } from './type-annotations.js'
21
-
22
- const JOJO_RUNTIME = 'jojoscript/runtime'
23
-
24
- export function compile(source) {
25
- let result = source
26
-
27
- /*
28
- * Optional type annotations, stripped and re-expressed as JSDoc before
29
- * anything else touches `fn` declarations.
30
- */
31
- result = transformTypeAnnotations(result)
32
-
33
- /*
34
- * Jojo functions.
35
- */
36
- result = transformExpressionFunctions(result)
37
- result = transformFunctionDeclarations(result)
38
-
39
- /*
40
- * `match` expressions.
41
- */
42
- result = transformMatchExpressions(result)
43
-
44
- /*
45
- * Jojo declarations.
46
- */
47
- result = transformMultiBindingDeclarations(result)
48
- result = transformDestructuringDeclarations(result)
49
- result = transformMutableDeclarations(result)
50
- result = transformConstDeclarations(result)
51
-
52
- /*
53
- * Remember which pipeline stages need the Jojo runtime before
54
- * the pipeline syntax itself is removed.
55
- */
56
- const pipelineStdlibFunctions =
57
- findPipelineStdlibFunctions(result)
58
-
59
- /*
60
- * Pipelines.
61
- */
62
- result = transformPipelines(result)
63
-
64
- /*
65
- * ES module imports.
66
- */
67
- result = transformLocalImports(result)
68
-
69
- /*
70
- * Generated JavaScript imports the runtime as a normal package.
71
- * This makes compiled applications portable and avoids globals. If the
72
- * source already has a manual `jojoscript/runtime` import (e.g. for a
73
- * stdlib function that is only ever called directly, never through a
74
- * pipeline stage), the auto-imported names are merged into it instead
75
- * of creating a second, colliding import declaration.
76
- */
77
- if (pipelineStdlibFunctions.length) {
78
- result = mergeRuntimeImport(result, pipelineStdlibFunctions)
79
- }
80
-
81
- return result.endsWith('\n') ? result : result + '\n'
82
- }
83
-
84
- const RUNTIME_IMPORT_PATTERN = /import\s*\{([^}]*)\}\s*from\s*(["'])jojoscript\/runtime\2/
85
-
86
- function mergeRuntimeImport(source, stdlibFunctions) {
87
- const existing = source.match(RUNTIME_IMPORT_PATTERN)
88
-
89
- if (!existing) {
90
- return `import { ${stdlibFunctions.join(', ')} } from "${JOJO_RUNTIME}"\n` + source
91
- }
92
-
93
- const existingNames = existing[1].split(',').map(name => name.trim()).filter(Boolean)
94
- const merged = [...new Set([...existingNames, ...stdlibFunctions])]
95
-
96
- return source.replace(
97
- RUNTIME_IMPORT_PATTERN,
98
- `import { ${merged.join(', ')} } from "${JOJO_RUNTIME}"`
99
- )
100
- }
101
-
102
- export function compileFile(input, outDir = null) {
103
- const src = path.resolve(input)
104
- const dir = outDir
105
- ? path.resolve(outDir)
106
- : path.join(path.dirname(src), 'dist')
107
-
108
- fs.mkdirSync(dir, { recursive: true })
109
-
110
- const out = path.join(
111
- dir,
112
- path.basename(src, path.extname(src)) + '.js'
113
- )
114
-
115
- fs.writeFileSync(
116
- out,
117
- compile(fs.readFileSync(src, 'utf8')),
118
- 'utf8'
119
- )
120
-
121
- return { input: src, output: out }
122
- }
123
-
124
- export function compileDirectory(inputDir, outDir = null) {
125
- const root = path.resolve(inputDir)
126
- const target = outDir
127
- ? path.resolve(outDir)
128
- : path.join(root, 'dist')
129
-
130
- const result = []
131
-
132
- function walk(dir) {
133
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
134
- if (
135
- entry.name === 'node_modules' ||
136
- entry.name === 'dist' ||
137
- entry.name.startsWith('.')
138
- ) {
139
- continue
140
- }
141
-
142
- const full = path.join(dir, entry.name)
143
-
144
- if (entry.isDirectory()) {
145
- walk(full)
146
- continue
147
- }
148
-
149
- if (!entry.name.endsWith('.jojo')) continue
150
-
151
- const rel =
152
- path.relative(root, full).slice(0, -'.jojo'.length) + '.js'
153
-
154
- const out = path.join(target, rel)
155
-
156
- fs.mkdirSync(path.dirname(out), { recursive: true })
157
- fs.writeFileSync(
158
- out,
159
- compile(fs.readFileSync(full, 'utf8')),
160
- 'utf8'
161
- )
162
-
163
- result.push({ input: full, output: out })
164
- }
165
- }
166
-
167
- walk(root)
168
- return result
169
- }
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
+
24
+ /*
25
+ * The auto-generated runtime import must use whatever name this package is
26
+ * actually installed under (e.g. a scoped fork like "@me/jojoscript"), not a
27
+ * hardcoded literal, so it's derived from this package's own package.json
28
+ * instead of being hardcoded.
29
+ */
30
+ const PACKAGE_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..')
31
+ const OWN_PACKAGE_NAME = JSON.parse(
32
+ fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf8')
33
+ ).name
34
+ const JOJO_RUNTIME = `${OWN_PACKAGE_NAME}/runtime`
35
+
36
+ export function compile(source, options = {}) {
37
+ let result = source
38
+ const runtimeImport = options.runtimeImport ?? JOJO_RUNTIME
39
+
40
+ /*
41
+ * Optional type annotations, stripped and re-expressed as JSDoc before
42
+ * anything else touches `fn` declarations.
43
+ */
44
+ result = transformTypeAnnotations(result)
45
+
46
+ /*
47
+ * Jojo functions.
48
+ */
49
+ result = transformExpressionFunctions(result)
50
+ result = transformFunctionDeclarations(result)
51
+
52
+ /*
53
+ * `match` expressions.
54
+ */
55
+ result = transformMatchExpressions(result)
56
+
57
+ /*
58
+ * Jojo declarations.
59
+ */
60
+ result = transformMultiBindingDeclarations(result)
61
+ result = transformDestructuringDeclarations(result)
62
+ result = transformMutableDeclarations(result)
63
+ result = transformConstDeclarations(result)
64
+
65
+ /*
66
+ * Remember which pipeline stages need the Jojo runtime before
67
+ * the pipeline syntax itself is removed.
68
+ */
69
+ const pipelineStdlibFunctions =
70
+ findPipelineStdlibFunctions(result)
71
+
72
+ /*
73
+ * `jojo profile` compiles with `{ profile: true }`, which wraps every
74
+ * pipeline stage (and the initial source expression) in a call to the
75
+ * `traceNode` runtime helper. That helper is never written by the user,
76
+ * so it must be added to the auto-import list explicitly.
77
+ */
78
+ if (options.profile && hasPipelines(result) && !pipelineStdlibFunctions.includes('traceNode')) {
79
+ pipelineStdlibFunctions.push('traceNode')
80
+ }
81
+
82
+ /*
83
+ * Pipelines.
84
+ */
85
+ result = transformPipelines(result, options)
86
+
87
+ /*
88
+ * ES module imports.
89
+ */
90
+ result = transformLocalImports(result)
91
+
92
+ /*
93
+ * Generated JavaScript imports the runtime as a normal package.
94
+ * This makes compiled applications portable and avoids globals. If the
95
+ * source already has a manual `jojoscript/runtime` import (e.g. for a
96
+ * stdlib function that is only ever called directly, never through a
97
+ * pipeline stage), the auto-imported names are merged into it instead
98
+ * of creating a second, colliding import declaration.
99
+ */
100
+ if (pipelineStdlibFunctions.length) {
101
+ result = mergeRuntimeImport(result, pipelineStdlibFunctions, runtimeImport)
102
+ }
103
+
104
+ return result.endsWith('\n') ? result : result + '\n'
105
+ }
106
+
107
+ function escapeRegExp(value) {
108
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
109
+ }
110
+
111
+ function mergeRuntimeImport(source, stdlibFunctions, runtimeImport) {
112
+ const pattern = new RegExp(
113
+ `import\\s*\\{([^}]*)\\}\\s*from\\s*(["'])([^"']+\\/runtime)\\2`
114
+ )
115
+ const existing = source.match(pattern)
116
+
117
+ if (!existing) {
118
+ return `import { ${stdlibFunctions.join(', ')} } from "${runtimeImport}"\n` + source
119
+ }
120
+
121
+ const existingNames = existing[1].split(',').map(name => name.trim()).filter(Boolean)
122
+ const merged = [...new Set([...existingNames, ...stdlibFunctions])]
123
+
124
+ return source.replace(
125
+ pattern,
126
+ `import { ${merged.join(', ')} } from "${runtimeImport}"`
127
+ )
128
+ }
129
+
130
+ export function compileFile(input, outDir = null, options = {}) {
131
+ const src = path.resolve(input)
132
+ const dir = outDir
133
+ ? path.resolve(outDir)
134
+ : path.join(path.dirname(src), 'dist')
135
+
136
+ fs.mkdirSync(dir, { recursive: true })
137
+
138
+ const out = path.join(
139
+ dir,
140
+ path.basename(src, path.extname(src)) + '.js'
141
+ )
142
+
143
+ fs.writeFileSync(
144
+ out,
145
+ compile(fs.readFileSync(src, 'utf8'), options),
146
+ 'utf8'
147
+ )
148
+
149
+ return { input: src, output: out }
150
+ }
151
+
152
+ export function compileDirectory(inputDir, outDir = null, options = {}) {
153
+ const root = path.resolve(inputDir)
154
+ const target = outDir
155
+ ? path.resolve(outDir)
156
+ : path.join(root, 'dist')
157
+
158
+ const result = []
159
+
160
+ function walk(dir) {
161
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
162
+ if (
163
+ entry.name === 'node_modules' ||
164
+ entry.name === 'dist' ||
165
+ entry.name.startsWith('.')
166
+ ) {
167
+ continue
168
+ }
169
+
170
+ const full = path.join(dir, entry.name)
171
+
172
+ if (entry.isDirectory()) {
173
+ walk(full)
174
+ continue
175
+ }
176
+
177
+ if (!entry.name.endsWith('.jojo')) continue
178
+
179
+ const rel =
180
+ path.relative(root, full).slice(0, -'.jojo'.length) + '.js'
181
+
182
+ const out = path.join(target, rel)
183
+
184
+ fs.mkdirSync(path.dirname(out), { recursive: true })
185
+ fs.writeFileSync(
186
+ out,
187
+ compile(fs.readFileSync(full, 'utf8'), options),
188
+ 'utf8'
189
+ )
190
+
191
+ result.push({ input: full, output: out })
192
+ }
193
+ }
194
+
195
+ walk(root)
196
+ return result
197
+ }
@@ -0,0 +1,27 @@
1
+ import { createHash } from 'node:crypto'
2
+
3
+ /*
4
+ * A pipeline's identity must be a deterministic hash of its *normalized*
5
+ * structure (stage names + arguments, whitespace-insensitive) rather than a
6
+ * random id, so the same program hashes to the same pipeline id across
7
+ * separate process runs. This is what lets checkpoint data saved by one run
8
+ * be found again by a later run (`jojo resume`, `jojo replay`).
9
+ */
10
+ export function computePipelineId(pipeline) {
11
+ const normalized = {
12
+ expression: normalizeText(pipeline.expression),
13
+ stages: pipeline.stages.map(stage => ({
14
+ name: stage.name,
15
+ args: stage.args.map(normalizeText)
16
+ }))
17
+ }
18
+
19
+ return createHash('sha256')
20
+ .update(JSON.stringify(normalized))
21
+ .digest('hex')
22
+ .slice(0, 16)
23
+ }
24
+
25
+ function normalizeText(text) {
26
+ return text.replace(/\s+/g, ' ').trim()
27
+ }
@@ -0,0 +1,100 @@
1
+ import { findAllPipelines } from './pipeline-parser.js'
2
+ import { computePipelineId } from './pipeline-hash.js'
3
+
4
+ /*
5
+ * Static, execution-free representation of a pipeline: SOURCE, one node per
6
+ * stage, RESULT, chained by `dependencies`. Used by `jojo graph` (pure
7
+ * text/DOT rendering, no execution) and correlated at runtime with
8
+ * `jojo profile`'s collected metrics via the same `node-<n>` id scheme the
9
+ * compiler uses when emitting code (see `pipeline-parser.js`'s
10
+ * `emitPipeline`).
11
+ */
12
+ export function buildGraph(pipeline, pipelineId = computePipelineId(pipeline)) {
13
+ const nodes = []
14
+
15
+ nodes.push({
16
+ id: 'node-0',
17
+ type: 'source',
18
+ label: pipeline.expression.trim(),
19
+ args: [],
20
+ dependencies: [],
21
+ metadata: {}
22
+ })
23
+
24
+ pipeline.stages.forEach((stage, index) => {
25
+ nodes.push({
26
+ id: `node-${index + 1}`,
27
+ type: stage.name,
28
+ label: stage.args.length ? `${stage.name}(${stage.args.join(', ')})` : stage.name,
29
+ args: stage.args,
30
+ dependencies: [`node-${index}`],
31
+ metadata: {}
32
+ })
33
+ })
34
+
35
+ const resultId = `node-${pipeline.stages.length + 1}`
36
+ nodes.push({
37
+ id: resultId,
38
+ type: 'result',
39
+ label: 'RESULT',
40
+ args: [],
41
+ dependencies: [`node-${pipeline.stages.length}`],
42
+ metadata: {}
43
+ })
44
+
45
+ return { id: pipelineId, expression: pipeline.expression.trim(), nodes }
46
+ }
47
+
48
+ /*
49
+ * Parses every pipeline statement in `source` and returns their graphs,
50
+ * each with a deterministic id derived from its normalized structure.
51
+ */
52
+ export function analyzePipelines(source) {
53
+ return findAllPipelines(source).map(pipeline => buildGraph(pipeline))
54
+ }
55
+
56
+ export function renderGraphText(graph) {
57
+ const lines = []
58
+
59
+ graph.nodes.forEach((node, index) => {
60
+ let label
61
+ if (node.type === 'source') {
62
+ label = `SOURCE ${node.label}`
63
+ } else if (node.type === 'result') {
64
+ label = 'RESULT'
65
+ } else {
66
+ label = node.args.length
67
+ ? `${node.type.toUpperCase()} ${node.args.join(', ')}`
68
+ : node.type.toUpperCase()
69
+ }
70
+
71
+ lines.push(label)
72
+
73
+ if (index < graph.nodes.length - 1) {
74
+ lines.push(' │')
75
+ lines.push(' ▼')
76
+ }
77
+ })
78
+
79
+ return lines.join('\n')
80
+ }
81
+
82
+ export function renderGraphDot(graph) {
83
+ const lines = ['digraph pipeline {']
84
+
85
+ for (const node of graph.nodes) {
86
+ const dotLabel = node.args.length
87
+ ? `${node.type}\\n${node.args.join(', ').replace(/"/g, '\\"')}`
88
+ : node.type
89
+ lines.push(` "${node.id}" [label="${dotLabel}"];`)
90
+ }
91
+
92
+ for (const node of graph.nodes) {
93
+ for (const dependency of node.dependencies) {
94
+ lines.push(` "${dependency}" -> "${node.id}";`)
95
+ }
96
+ }
97
+
98
+ lines.push('}')
99
+ return lines.join('\n')
100
+ }