@panagos/jojoscript 0.7.1 → 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.1",
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,186 +1,197 @@
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
- } from './pipeline-parser.js'
9
-
10
- import {
11
- transformMutableDeclarations,
12
- transformConstDeclarations,
13
- transformMultiBindingDeclarations,
14
- transformDestructuringDeclarations,
15
- transformFunctionDeclarations,
16
- transformExpressionFunctions,
17
- transformLocalImports
18
- } from './transforms.js'
19
-
20
- import { transformMatchExpressions } from './match-parser.js'
21
- import { transformTypeAnnotations } from './type-annotations.js'
22
-
23
- /*
24
- * The auto-generated runtime import must use whatever name this package is
25
- * actually installed under (e.g. a scoped fork like "@me/jojoscript"), not a
26
- * hardcoded literal, so it's derived from this package's own package.json
27
- * instead of being hardcoded.
28
- */
29
- const PACKAGE_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..')
30
- const OWN_PACKAGE_NAME = JSON.parse(
31
- fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf8')
32
- ).name
33
- const JOJO_RUNTIME = `${OWN_PACKAGE_NAME}/runtime`
34
-
35
- export function compile(source) {
36
- let result = source
37
-
38
- /*
39
- * Optional type annotations, stripped and re-expressed as JSDoc before
40
- * anything else touches `fn` declarations.
41
- */
42
- result = transformTypeAnnotations(result)
43
-
44
- /*
45
- * Jojo functions.
46
- */
47
- result = transformExpressionFunctions(result)
48
- result = transformFunctionDeclarations(result)
49
-
50
- /*
51
- * `match` expressions.
52
- */
53
- result = transformMatchExpressions(result)
54
-
55
- /*
56
- * Jojo declarations.
57
- */
58
- result = transformMultiBindingDeclarations(result)
59
- result = transformDestructuringDeclarations(result)
60
- result = transformMutableDeclarations(result)
61
- result = transformConstDeclarations(result)
62
-
63
- /*
64
- * Remember which pipeline stages need the Jojo runtime before
65
- * the pipeline syntax itself is removed.
66
- */
67
- const pipelineStdlibFunctions =
68
- findPipelineStdlibFunctions(result)
69
-
70
- /*
71
- * Pipelines.
72
- */
73
- result = transformPipelines(result)
74
-
75
- /*
76
- * ES module imports.
77
- */
78
- result = transformLocalImports(result)
79
-
80
- /*
81
- * Generated JavaScript imports the runtime as a normal package.
82
- * This makes compiled applications portable and avoids globals. If the
83
- * source already has a manual `jojoscript/runtime` import (e.g. for a
84
- * stdlib function that is only ever called directly, never through a
85
- * pipeline stage), the auto-imported names are merged into it instead
86
- * of creating a second, colliding import declaration.
87
- */
88
- if (pipelineStdlibFunctions.length) {
89
- result = mergeRuntimeImport(result, pipelineStdlibFunctions)
90
- }
91
-
92
- return result.endsWith('\n') ? result : result + '\n'
93
- }
94
-
95
- const RUNTIME_IMPORT_PATTERN = new RegExp(
96
- `import\\s*\\{([^}]*)\\}\\s*from\\s*(["'])${escapeRegExp(JOJO_RUNTIME)}\\2`
97
- )
98
-
99
- function escapeRegExp(value) {
100
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
101
- }
102
-
103
- function mergeRuntimeImport(source, stdlibFunctions) {
104
- const existing = source.match(RUNTIME_IMPORT_PATTERN)
105
-
106
- if (!existing) {
107
- return `import { ${stdlibFunctions.join(', ')} } from "${JOJO_RUNTIME}"\n` + source
108
- }
109
-
110
- const existingNames = existing[1].split(',').map(name => name.trim()).filter(Boolean)
111
- const merged = [...new Set([...existingNames, ...stdlibFunctions])]
112
-
113
- return source.replace(
114
- RUNTIME_IMPORT_PATTERN,
115
- `import { ${merged.join(', ')} } from "${JOJO_RUNTIME}"`
116
- )
117
- }
118
-
119
- export function compileFile(input, outDir = null) {
120
- const src = path.resolve(input)
121
- const dir = outDir
122
- ? path.resolve(outDir)
123
- : path.join(path.dirname(src), 'dist')
124
-
125
- fs.mkdirSync(dir, { recursive: true })
126
-
127
- const out = path.join(
128
- dir,
129
- path.basename(src, path.extname(src)) + '.js'
130
- )
131
-
132
- fs.writeFileSync(
133
- out,
134
- compile(fs.readFileSync(src, 'utf8')),
135
- 'utf8'
136
- )
137
-
138
- return { input: src, output: out }
139
- }
140
-
141
- export function compileDirectory(inputDir, outDir = null) {
142
- const root = path.resolve(inputDir)
143
- const target = outDir
144
- ? path.resolve(outDir)
145
- : path.join(root, 'dist')
146
-
147
- const result = []
148
-
149
- function walk(dir) {
150
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
151
- if (
152
- entry.name === 'node_modules' ||
153
- entry.name === 'dist' ||
154
- entry.name.startsWith('.')
155
- ) {
156
- continue
157
- }
158
-
159
- const full = path.join(dir, entry.name)
160
-
161
- if (entry.isDirectory()) {
162
- walk(full)
163
- continue
164
- }
165
-
166
- if (!entry.name.endsWith('.jojo')) continue
167
-
168
- const rel =
169
- path.relative(root, full).slice(0, -'.jojo'.length) + '.js'
170
-
171
- const out = path.join(target, rel)
172
-
173
- fs.mkdirSync(path.dirname(out), { recursive: true })
174
- fs.writeFileSync(
175
- out,
176
- compile(fs.readFileSync(full, 'utf8')),
177
- 'utf8'
178
- )
179
-
180
- result.push({ input: full, output: out })
181
- }
182
- }
183
-
184
- walk(root)
185
- return result
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
+
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
+ }