@panagos/jojoscript 0.10.1 → 1.0.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 +677 -11
- package/bin/jojo.js +257 -23
- package/package.json +1 -1
- package/src/check.js +253 -0
- package/src/compiler.js +36 -37
- package/src/contracts.js +124 -0
- package/src/describe.js +63 -0
- package/src/effect-parser.js +4 -2
- package/src/formatter.js +105 -0
- package/src/index.js +31 -0
- package/src/match-parser.js +212 -14
- package/src/named-pipelines.js +87 -0
- package/src/parse.js +124 -0
- package/src/pipeline-explain.js +132 -0
- package/src/pipeline-ir.js +9 -7
- package/src/pipeline-parser.js +24 -2
- package/src/pipeline-plan.js +203 -0
- package/src/purity.js +101 -0
- package/src/runtime/checkpoint-store.js +5 -3
- package/src/runtime/checkpoint.js +3 -2
- package/src/runtime/effects.js +73 -3
- package/src/runtime/errors.js +14 -0
- package/src/runtime/index.js +8 -2
- package/src/runtime/mcp.js +1 -1
- package/src/runtime/serialization.js +97 -0
- package/src/runtime/tagged-unions.js +29 -0
- package/src/simulate.js +156 -0
- package/src/type-declarations.js +83 -0
- package/src/why.js +146 -0
package/src/compiler.js
CHANGED
|
@@ -20,6 +20,10 @@ import {
|
|
|
20
20
|
|
|
21
21
|
import { transformMatchExpressions } from './match-parser.js'
|
|
22
22
|
import { transformTypeAnnotations } from './type-annotations.js'
|
|
23
|
+
import { transformPurityModifiers } from './purity.js'
|
|
24
|
+
import { transformNamedPipelines } from './named-pipelines.js'
|
|
25
|
+
import { transformContracts, findContracts } from './contracts.js'
|
|
26
|
+
import { transformTypeDeclarations, findTypeDeclarations } from './type-declarations.js'
|
|
23
27
|
import {
|
|
24
28
|
transformEffectDeclarations,
|
|
25
29
|
transformHandleBlocks,
|
|
@@ -47,8 +51,16 @@ export function compile(source, options = {}) {
|
|
|
47
51
|
* Optional type annotations, stripped and re-expressed as JSDoc before
|
|
48
52
|
* anything else touches `fn` declarations.
|
|
49
53
|
*/
|
|
54
|
+
result = transformNamedPipelines(result)
|
|
55
|
+
result = transformPurityModifiers(result)
|
|
56
|
+
|
|
57
|
+
const usesContracts = findContracts(result).length > 0
|
|
58
|
+
result = transformContracts(result)
|
|
50
59
|
result = transformTypeAnnotations(result)
|
|
51
60
|
|
|
61
|
+
const usesTypeDeclarations = findTypeDeclarations(result).length > 0
|
|
62
|
+
result = transformTypeDeclarations(result)
|
|
63
|
+
|
|
52
64
|
/*
|
|
53
65
|
* Jojo functions.
|
|
54
66
|
*/
|
|
@@ -98,6 +110,12 @@ export function compile(source, options = {}) {
|
|
|
98
110
|
if (usesHandleBlocks && !pipelineStdlibFunctions.includes('withHandlers')) {
|
|
99
111
|
pipelineStdlibFunctions.push('withHandlers')
|
|
100
112
|
}
|
|
113
|
+
if (usesContracts && !pipelineStdlibFunctions.includes('ContractError')) {
|
|
114
|
+
pipelineStdlibFunctions.push('ContractError')
|
|
115
|
+
}
|
|
116
|
+
if (usesTypeDeclarations && !pipelineStdlibFunctions.includes('defineType')) {
|
|
117
|
+
pipelineStdlibFunctions.push('defineType')
|
|
118
|
+
}
|
|
101
119
|
|
|
102
120
|
/*
|
|
103
121
|
* `jojo profile` compiles with `{ profile: true }`, which wraps every
|
|
@@ -127,17 +145,13 @@ export function compile(source, options = {}) {
|
|
|
127
145
|
* pipeline stage), the auto-imported names are merged into it instead
|
|
128
146
|
* of creating a second, colliding import declaration.
|
|
129
147
|
*/
|
|
130
|
-
if (pipelineStdlibFunctions.length) {
|
|
148
|
+
if (pipelineStdlibFunctions.length || options.runtimeImport) {
|
|
131
149
|
result = mergeRuntimeImport(result, pipelineStdlibFunctions, runtimeImport)
|
|
132
150
|
}
|
|
133
151
|
|
|
134
152
|
return result.endsWith('\n') ? result : result + '\n'
|
|
135
153
|
}
|
|
136
154
|
|
|
137
|
-
function escapeRegExp(value) {
|
|
138
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
139
|
-
}
|
|
140
|
-
|
|
141
155
|
function mergeRuntimeImport(source, stdlibFunctions, runtimeImport) {
|
|
142
156
|
const pattern = new RegExp(
|
|
143
157
|
`import\\s*\\{([^}]*)\\}\\s*from\\s*(["'])([^"']+\\/runtime)\\2`
|
|
@@ -145,6 +159,7 @@ function mergeRuntimeImport(source, stdlibFunctions, runtimeImport) {
|
|
|
145
159
|
const existing = source.match(pattern)
|
|
146
160
|
|
|
147
161
|
if (!existing) {
|
|
162
|
+
if (!stdlibFunctions.length) return source
|
|
148
163
|
return `import { ${stdlibFunctions.join(', ')} } from "${runtimeImport}"\n` + source
|
|
149
164
|
}
|
|
150
165
|
|
|
@@ -185,43 +200,27 @@ export function compileDirectory(inputDir, outDir = null, options = {}) {
|
|
|
185
200
|
? path.resolve(outDir)
|
|
186
201
|
: path.join(root, 'dist')
|
|
187
202
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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)
|
|
203
|
+
return findJojoFiles(root).map(full => {
|
|
204
|
+
const rel = path.relative(root, full).slice(0, -'.jojo'.length) + '.js'
|
|
205
|
+
const out = path.join(target, rel)
|
|
201
206
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
continue
|
|
205
|
-
}
|
|
207
|
+
fs.mkdirSync(path.dirname(out), { recursive: true })
|
|
208
|
+
fs.writeFileSync(out, compile(fs.readFileSync(full, 'utf8'), options), 'utf8')
|
|
206
209
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
path.relative(root, full).slice(0, -'.jojo'.length) + '.js'
|
|
210
|
+
return { input: full, output: out }
|
|
211
|
+
})
|
|
212
|
+
}
|
|
211
213
|
|
|
212
|
-
|
|
214
|
+
export function findJojoFiles(root) {
|
|
215
|
+
const files = []
|
|
213
216
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
out,
|
|
217
|
-
compile(fs.readFileSync(full, 'utf8'), options),
|
|
218
|
-
'utf8'
|
|
219
|
-
)
|
|
217
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
218
|
+
if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name.startsWith('.')) continue
|
|
220
219
|
|
|
221
|
-
|
|
222
|
-
|
|
220
|
+
const full = path.join(root, entry.name)
|
|
221
|
+
if (entry.isDirectory()) files.push(...findJojoFiles(full))
|
|
222
|
+
else if (entry.name.endsWith('.jojo')) files.push(full)
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
-
|
|
226
|
-
return result
|
|
225
|
+
return files
|
|
227
226
|
}
|
package/src/contracts.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { maskNonCode } from './lexer.js'
|
|
2
|
+
|
|
3
|
+
const FUNCTION_HEAD = /^[ \t]*(?:export[ \t]+)?(?:(?:pure|effectful)[ \t]+)?(async[ \t]+)?(?:(?:pure|effectful)[ \t]+)?fn[ \t]+([A-Za-z_$][\w$]*)[ \t]*\(/gm
|
|
4
|
+
const CLAUSE = /^([ \t]*)(requires|ensures)[ \t]+(.+?)[ \t]*$/
|
|
5
|
+
|
|
6
|
+
function lineAt(source, offset) {
|
|
7
|
+
return source.slice(0, offset).split('\n').length
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function matchingClose(code, openIndex, open, close) {
|
|
11
|
+
let depth = 0
|
|
12
|
+
for (let i = openIndex; i < code.length; i++) {
|
|
13
|
+
if (code[i] === open) depth++
|
|
14
|
+
else if (code[i] === close && --depth === 0) return i
|
|
15
|
+
}
|
|
16
|
+
return -1
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parameterNames(params) {
|
|
20
|
+
return params.split(',')
|
|
21
|
+
.map(param => param.trim().match(/^(?:\.\.\.)?([A-Za-z_$][\w$]*)/)?.[1])
|
|
22
|
+
.filter(Boolean)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/*
|
|
26
|
+
* Contract clauses sit on their own lines between a block function's
|
|
27
|
+
* parameter list and its opening `{`:
|
|
28
|
+
*
|
|
29
|
+
* fn divide(a, b)
|
|
30
|
+
* requires b != 0
|
|
31
|
+
* ensures result * b == a
|
|
32
|
+
* {
|
|
33
|
+
*/
|
|
34
|
+
export function findContracts(source) {
|
|
35
|
+
const code = maskNonCode(source)
|
|
36
|
+
const regex = new RegExp(FUNCTION_HEAD.source, FUNCTION_HEAD.flags)
|
|
37
|
+
const contracts = []
|
|
38
|
+
let match
|
|
39
|
+
|
|
40
|
+
while ((match = regex.exec(code))) {
|
|
41
|
+
const paramsOpen = match.index + match[0].length - 1
|
|
42
|
+
const paramsClose = matchingClose(code, paramsOpen, '(', ')')
|
|
43
|
+
if (paramsClose === -1) continue
|
|
44
|
+
|
|
45
|
+
const headEnd = code.indexOf('\n', paramsClose)
|
|
46
|
+
if (headEnd === -1 || /[{=]/.test(code.slice(paramsClose + 1, headEnd))) continue
|
|
47
|
+
|
|
48
|
+
const clauses = []
|
|
49
|
+
let position = headEnd + 1
|
|
50
|
+
let bodyOpen = -1
|
|
51
|
+
|
|
52
|
+
while (position < code.length) {
|
|
53
|
+
const lineEnd = code.indexOf('\n', position) === -1 ? code.length : code.indexOf('\n', position)
|
|
54
|
+
const line = source.slice(position, lineEnd)
|
|
55
|
+
const clause = code.slice(position, lineEnd).match(CLAUSE)
|
|
56
|
+
|
|
57
|
+
if (clause) {
|
|
58
|
+
clauses.push({
|
|
59
|
+
kind: clause[2],
|
|
60
|
+
condition: line.slice(clause[1].length + clause[2].length).trim(),
|
|
61
|
+
line: lineAt(source, position),
|
|
62
|
+
start: position,
|
|
63
|
+
end: lineEnd
|
|
64
|
+
})
|
|
65
|
+
} else if (line.trim().startsWith('{')) {
|
|
66
|
+
bodyOpen = position + line.indexOf('{')
|
|
67
|
+
break
|
|
68
|
+
} else {
|
|
69
|
+
break
|
|
70
|
+
}
|
|
71
|
+
position = lineEnd + 1
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!clauses.length || bodyOpen === -1) continue
|
|
75
|
+
|
|
76
|
+
const toEntry = ({ condition, line }) => ({ condition, line })
|
|
77
|
+
contracts.push({
|
|
78
|
+
name: match[2],
|
|
79
|
+
async: Boolean(match[1]),
|
|
80
|
+
params: parameterNames(source.slice(paramsOpen + 1, paramsClose)),
|
|
81
|
+
line: lineAt(source, match.index + match[0].search(/\S/)),
|
|
82
|
+
requires: clauses.filter(clause => clause.kind === 'requires').map(toEntry),
|
|
83
|
+
ensures: clauses.filter(clause => clause.kind === 'ensures').map(toEntry),
|
|
84
|
+
clauses,
|
|
85
|
+
bodyOpen,
|
|
86
|
+
bodyClose: matchingClose(code, bodyOpen, '{', '}')
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return contracts
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function check(contract, { kind, condition }) {
|
|
94
|
+
const error = `new ContractError(${JSON.stringify(contract.name)}, "${kind}", ${JSON.stringify(condition)})`
|
|
95
|
+
return `if (!(${condition})) throw ${error};`
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/*
|
|
99
|
+
* The body runs inside an arrow function so `ensures` can inspect `result`
|
|
100
|
+
* while `this` and `arguments` still refer to the declared function.
|
|
101
|
+
*/
|
|
102
|
+
export function transformContracts(source) {
|
|
103
|
+
let result = source
|
|
104
|
+
|
|
105
|
+
for (const contract of findContracts(source).reverse()) {
|
|
106
|
+
const requires = contract.clauses.filter(clause => clause.kind === 'requires').map(clause => check(contract, clause))
|
|
107
|
+
const ensures = contract.clauses.filter(clause => clause.kind === 'ensures').map(clause => check(contract, clause))
|
|
108
|
+
|
|
109
|
+
let open = `{ ${requires.join(' ')}`
|
|
110
|
+
let close = '}'
|
|
111
|
+
if (ensures.length) {
|
|
112
|
+
open += contract.async ? ' const result = await (async () => {' : ' const result = (() => {'
|
|
113
|
+
close = `})(); ${ensures.join(' ')} return result }`
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
result = result.slice(0, contract.bodyClose) + close + result.slice(contract.bodyClose + 1)
|
|
117
|
+
result = result.slice(0, contract.bodyOpen) + open.trimEnd() + result.slice(contract.bodyOpen + 1)
|
|
118
|
+
for (const clause of [...contract.clauses].reverse()) {
|
|
119
|
+
result = result.slice(0, clause.start) + result.slice(clause.end)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return result
|
|
124
|
+
}
|
package/src/describe.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { explainPipelines } from './pipeline-explain.js'
|
|
2
|
+
import { planPipelines } from './pipeline-plan.js'
|
|
3
|
+
import { findEffectDeclarations, findHandleBlocks } from './effect-parser.js'
|
|
4
|
+
import { findPurityDeclarations } from './purity.js'
|
|
5
|
+
import { findContracts } from './contracts.js'
|
|
6
|
+
import { findTypeDeclarations } from './type-declarations.js'
|
|
7
|
+
|
|
8
|
+
const DESCRIPTION_VERSION = '1'
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
* One self-describing JSON document for a source file, assembled from the
|
|
12
|
+
* same analyses as `explain`, `plan`, and `check` so they never disagree.
|
|
13
|
+
*/
|
|
14
|
+
export function describeSource(source) {
|
|
15
|
+
const plans = planPipelines(source).pipelines
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
language: 'jojo',
|
|
19
|
+
version: DESCRIPTION_VERSION,
|
|
20
|
+
pipelines: explainPipelines(source).map((explanation, index) => describePipeline(explanation, plans[index])),
|
|
21
|
+
effects: describeEffects(source),
|
|
22
|
+
functions: describeFunctions(source),
|
|
23
|
+
types: findTypeDeclarations(source).map(({ name, variants }) => ({ name, variants }))
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function describePipeline(explanation, plan) {
|
|
28
|
+
const [source, ...stages] = explanation.stages
|
|
29
|
+
const effects = stages.flatMap(stage => [...stage.effects, ...stage.effectfulFunctions])
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
name: explanation.name,
|
|
33
|
+
inputs: /^[A-Za-z_$][\w$]*$/.test(source.detail) ? [source.detail] : [],
|
|
34
|
+
effects: [...new Set(effects)],
|
|
35
|
+
stages: stages.map((stage, index) => {
|
|
36
|
+
const { type, id, ...fields } = plan.stages[index + 1]
|
|
37
|
+
return { id: stage.id ?? `node-${stage.index - 1}`, operation: type, ...fields }
|
|
38
|
+
}),
|
|
39
|
+
properties: explanation.properties
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function describeEffects(source) {
|
|
44
|
+
const handled = new Set(findHandleBlocks(source).flatMap(block => block.handlerNames))
|
|
45
|
+
return findEffectDeclarations(source).map(({ name, params }) => ({ name, params, handled: handled.has(name) }))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function describeFunctions(source) {
|
|
49
|
+
const functions = new Map()
|
|
50
|
+
const entry = (name, line) => {
|
|
51
|
+
if (!functions.has(name)) functions.set(name, { name, line, purity: null, requires: [], ensures: [] })
|
|
52
|
+
return functions.get(name)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const { name, line, purity } of findPurityDeclarations(source)) entry(name, line).purity = purity
|
|
56
|
+
for (const { name, line, requires, ensures } of findContracts(source)) {
|
|
57
|
+
const described = entry(name, line)
|
|
58
|
+
described.requires = requires.map(clause => clause.condition)
|
|
59
|
+
described.ensures = ensures.map(clause => clause.condition)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return [...functions.values()].sort((a, b) => a.line - b.line)
|
|
63
|
+
}
|
package/src/effect-parser.js
CHANGED
|
@@ -211,11 +211,13 @@ export function transformHandleBlocks(source) {
|
|
|
211
211
|
|
|
212
212
|
const handlerText = text(result, tokens.slice(target.handlerOpen, target.handlerClose + 1))
|
|
213
213
|
const bodyText = text(result, tokens.slice(target.bodyOpen, target.bodyClose + 1))
|
|
214
|
-
const replacement = `(await withHandlers(${handlerText}, async () => ${bodyText}))`
|
|
215
|
-
|
|
216
214
|
const start = tokens[target.keywordIndex].start
|
|
217
215
|
const end = tokens[target.bodyClose].end
|
|
218
216
|
|
|
217
|
+
// A statement starting with `(` would otherwise be parsed as a call on the previous line.
|
|
218
|
+
const startsStatement = /(^|\n)[ \t]*$/.test(result.slice(0, start))
|
|
219
|
+
const replacement = `${startsStatement ? ';' : ''}(await withHandlers(${handlerText}, async () => ${bodyText}))`
|
|
220
|
+
|
|
219
221
|
result = result.slice(0, start) + replacement + result.slice(end)
|
|
220
222
|
}
|
|
221
223
|
|
package/src/formatter.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* A line-based reindenter, matching the compiler's no-AST design: it only
|
|
3
|
+
* normalizes indentation and whitespace and never rewrites expressions.
|
|
4
|
+
* The VS Code extension ships an identical copy (editors/vscode/src/formatter.mjs).
|
|
5
|
+
*
|
|
6
|
+
* Every open bracket remembers the indent level of the line that opened it,
|
|
7
|
+
* so a line opening several brackets (`reduce((a, b) => {`) indents its
|
|
8
|
+
* contents once. Block comments and template literal text are left
|
|
9
|
+
* untouched. Like the compiler, it is not regex-literal aware.
|
|
10
|
+
*/
|
|
11
|
+
const PIPELINE_BLOCK = /^(?:export\s+)?pipeline\s+[A-Za-z_$][\w$]*\s*\{$/
|
|
12
|
+
|
|
13
|
+
function continuationLevels(trimmed, insidePipelineBlock) {
|
|
14
|
+
if (trimmed.startsWith('|>')) return insidePipelineBlock ? 0 : 1
|
|
15
|
+
if (/^(requires|ensures)\b/.test(trimmed)) return 1
|
|
16
|
+
if (/^\|(?![|>])/.test(trimmed)) return 1
|
|
17
|
+
if (/^[?:](?![?:.])/.test(trimmed)) return 1
|
|
18
|
+
if (/^\.(?!\.)/.test(trimmed)) return 1
|
|
19
|
+
return 0
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function formatSource(text, options = {}) {
|
|
23
|
+
const indentUnit = options.indent ?? ' '
|
|
24
|
+
const stack = []
|
|
25
|
+
const output = []
|
|
26
|
+
let inBlockComment = false
|
|
27
|
+
let hangingDepth = null
|
|
28
|
+
|
|
29
|
+
const openBrackets = () => stack.filter(entry => entry.ch !== 'TPL')
|
|
30
|
+
|
|
31
|
+
function scanLine(line, level) {
|
|
32
|
+
let i = 0
|
|
33
|
+
while (i < line.length) {
|
|
34
|
+
if (inBlockComment) {
|
|
35
|
+
const end = line.indexOf('*/', i)
|
|
36
|
+
if (end === -1) return
|
|
37
|
+
inBlockComment = false
|
|
38
|
+
i = end + 2
|
|
39
|
+
continue
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const ch = line[i]
|
|
43
|
+
if (stack.at(-1)?.ch === 'TPL') {
|
|
44
|
+
if (ch === '\\') i += 2
|
|
45
|
+
else if (ch === '`') { stack.pop(); i++ }
|
|
46
|
+
else if (ch === '$' && line[i + 1] === '{') { stack.push({ ch: '{', level }); i += 2 }
|
|
47
|
+
else i++
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const two = line.slice(i, i + 2)
|
|
52
|
+
if (two === '//') return
|
|
53
|
+
if (two === '/*') { inBlockComment = true; i += 2; continue }
|
|
54
|
+
if (ch === '`') { stack.push({ ch: 'TPL' }); i++; continue }
|
|
55
|
+
|
|
56
|
+
if (ch === '"' || ch === "'") {
|
|
57
|
+
i++
|
|
58
|
+
while (i < line.length && line[i] !== ch) i += line[i] === '\\' ? 2 : 1
|
|
59
|
+
i++
|
|
60
|
+
continue
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if ('{[('.includes(ch)) stack.push({ ch, level, pipeline: ch === '{' && PIPELINE_BLOCK.test(line.trim()) })
|
|
64
|
+
else if ('}])'.includes(ch) && stack.length && stack.at(-1).ch !== 'TPL') stack.pop()
|
|
65
|
+
i++
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (const rawLine of text.split(/\r\n|\r|\n/)) {
|
|
70
|
+
const trimmed = rawLine.trim()
|
|
71
|
+
|
|
72
|
+
if (trimmed === '') {
|
|
73
|
+
output.push('')
|
|
74
|
+
hangingDepth = null
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (inBlockComment || stack.at(-1)?.ch === 'TPL') {
|
|
79
|
+
output.push(rawLine)
|
|
80
|
+
scanLine(rawLine, 0)
|
|
81
|
+
continue
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const open = openBrackets()
|
|
85
|
+
const innermost = open.at(-1)
|
|
86
|
+
const startsWithCloser = /^[}\])]/.test(trimmed)
|
|
87
|
+
|
|
88
|
+
let level = innermost ? innermost.level + (startsWithCloser ? 0 : 1) : 0
|
|
89
|
+
if (!startsWithCloser) level += continuationLevels(trimmed, innermost?.pipeline)
|
|
90
|
+
if (hangingDepth === open.length && !startsWithCloser) level += 1
|
|
91
|
+
|
|
92
|
+
output.push(indentUnit.repeat(level) + trimmed)
|
|
93
|
+
|
|
94
|
+
if (startsWithCloser || /\{$/.test(trimmed)) hangingDepth = null
|
|
95
|
+
if (/(:=|=>)$/.test(trimmed)) hangingDepth = open.length
|
|
96
|
+
|
|
97
|
+
scanLine(rawLine, level)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const collapsed = output.filter((line, index) => line !== '' || output[index - 1] !== '')
|
|
101
|
+
while (collapsed[0] === '') collapsed.shift()
|
|
102
|
+
while (collapsed.at(-1) === '') collapsed.pop()
|
|
103
|
+
|
|
104
|
+
return collapsed.join('\n') + '\n'
|
|
105
|
+
}
|
package/src/index.js
CHANGED
|
@@ -25,9 +25,40 @@ export {
|
|
|
25
25
|
renderGraphDot
|
|
26
26
|
} from './pipeline-ir.js'
|
|
27
27
|
|
|
28
|
+
export {
|
|
29
|
+
explainPipelines,
|
|
30
|
+
renderExplanationText
|
|
31
|
+
} from './pipeline-explain.js'
|
|
32
|
+
|
|
33
|
+
export {
|
|
34
|
+
planPipelines,
|
|
35
|
+
planToJojo
|
|
36
|
+
} from './pipeline-plan.js'
|
|
37
|
+
|
|
28
38
|
export {
|
|
29
39
|
transformEffectDeclarations,
|
|
30
40
|
transformHandleBlocks,
|
|
31
41
|
findEffectDeclarations,
|
|
32
42
|
findHandleBlocks
|
|
33
43
|
} from './effect-parser.js'
|
|
44
|
+
|
|
45
|
+
export {
|
|
46
|
+
checkSource,
|
|
47
|
+
checkPlan
|
|
48
|
+
} from './check.js'
|
|
49
|
+
|
|
50
|
+
export { describeSource } from './describe.js'
|
|
51
|
+
|
|
52
|
+
export {
|
|
53
|
+
simulatePipelines,
|
|
54
|
+
renderSimulationText
|
|
55
|
+
} from './simulate.js'
|
|
56
|
+
|
|
57
|
+
export {
|
|
58
|
+
whyPipelines,
|
|
59
|
+
renderWhyText
|
|
60
|
+
} from './why.js'
|
|
61
|
+
|
|
62
|
+
export { formatSource } from './formatter.js'
|
|
63
|
+
|
|
64
|
+
export { parseSource } from './parse.js'
|